refactor: lazy init global regexp variables (#40269)

Gitea binary is an all-in-one program.

It doesn't make sense to compile dozens or even hundreds of regexps for
git hook sub commands.
This commit is contained in:
wxiaoguang
2026-11-07 11:56:56 +00:00
committed by GitHub
parent 6bc92f464a
commit a165fdb63c
52 changed files with 474 additions and 282 deletions

View File

@@ -16,6 +16,7 @@ import (
"regexp"
"slices"
"strings"
"sync"
"gitea.dev/modules/json"
)
@@ -31,7 +32,9 @@ type emoji struct {
description string
}
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
var slugRe = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`[^a-z0-9]+`)
})
func main() {
if err := generate(); err != nil {
@@ -108,7 +111,7 @@ func fetchEmojis(existingAliases map[string][]string) ([]*emoji, error) {
for _, e := range emojis {
e.Aliases = existingAliases[e.Emoji]
if e.Aliases == nil {
e.Aliases = []string{strings.Trim(slugRe.ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
e.Aliases = []string{strings.Trim(slugRe().ReplaceAllString(strings.ReplaceAll(strings.ToLower(e.description), "’", ""), "_"), "_")}
}
delete(existingAliases, e.Emoji)
for _, alias := range e.Aliases {

View File

@@ -15,17 +15,24 @@ import (
"slices"
"sort"
"strings"
"sync"
)
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
var globalVars = sync.OnceValue(func() (ret struct {
licenseRe *regexp.Regexp
primaryLicenseRe *regexp.Regexp
},
) {
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
ret.licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
ret.primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
return ret
})
// ignoredNames are LicenseEntry.Name values to exclude from the output.
var ignoredNames = map[string]bool{
@@ -144,7 +151,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
continue
}
name := entry.Name()
if !licenseRe.MatchString(name) {
if !globalVars().licenseRe.MatchString(name) {
continue
}
if excludedExt[strings.ToLower(filepath.Ext(name))] {
@@ -181,7 +188,7 @@ func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
var primary []LicenseEntry
for _, e := range entries {
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
if primaryLicenseRe.MatchString(fileName) {
if globalVars().primaryLicenseRe.MatchString(fileName) {
primary = append(primary, e)
}
}

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"gitea.dev/modules/json"
@@ -16,11 +17,19 @@ import (
"github.com/getkin/kin-openapi/openapi3"
)
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
var globalVars = sync.OnceValue(func() (ret struct {
rxDeprecated *regexp.Regexp
rxSwaggerEnum *regexp.Regexp
},
) {
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
ret.rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
ret.rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
return ret
})
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
// Gitea-specific post-processing: file-schema fixups, URI formats,
@@ -158,7 +167,7 @@ func addDeprecatedFlags(doc *openapi3.T) {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if rxDeprecated.MatchString(propRef.Value.Description) {
if globalVars().rxDeprecated.MatchString(propRef.Value.Description) {
propRef.Value.Deprecated = true
}
}

View File

@@ -13,7 +13,6 @@ import (
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -31,8 +30,6 @@ func EnumKey(values []any) string {
return strings.Join(strs, "|")
}
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type names declared with
// // swagger:enum TypeName. Multiple type names per key are allowed (e.g.
@@ -146,7 +143,7 @@ func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes m
if doc == nil {
return nil
}
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
matches := globalVars().rxSwaggerEnum.FindStringSubmatch(doc.Text())
if len(matches) < 2 {
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"gitea.dev/models/db"
project_model "gitea.dev/models/project"
@@ -112,10 +113,15 @@ type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}
var (
issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
)
var globalVars = sync.OnceValue(func() (ret struct {
issueTasksPat *regexp.Regexp
issueTasksDonePat *regexp.Regexp
},
) {
ret.issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
ret.issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
return ret
})
// IssueIndex represents the issue index table
type IssueIndex db.ResourceIndex
@@ -463,12 +469,12 @@ func (issue *Issue) IsPoster(uid int64) bool {
// GetTasks returns the amount of tasks in the issues content
func (issue *Issue) GetTasks() int {
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
return len(globalVars().issueTasksPat.FindAllStringIndex(issue.Content, -1))
}
// GetTasksDone returns the amount of completed tasks in the issues content
func (issue *Issue) GetTasksDone() int {
return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
return len(globalVars().issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
}
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"math"
"regexp"
"sync"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
@@ -36,10 +37,12 @@ const (
)
// ColumnColorPattern is a regexp witch can validate ColumnColor
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
var ColumnColorPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile("^#[0-9a-fA-F]{6}$")
})
func validateColumnColor(color string) error {
if len(color) != 0 && !ColumnColorPattern.MatchString(color) {
if len(color) != 0 && !ColumnColorPattern().MatchString(color) {
return util.ErrorWrap(util.ErrUnprocessableContent, "invalid column color %q, expected a 6-digit hex string like #FF0000", color)
}
return nil

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"strings"
"sync"
"gitea.dev/models/db"
"gitea.dev/modules/container"
@@ -22,7 +23,9 @@ func init() {
db.RegisterModel(new(RepoTopic))
}
var topicPattern = regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
var topicPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^[a-z0-9][-.a-z0-9]*$`)
})
// Topic represents a topic of repositories
type Topic struct {
@@ -55,7 +58,7 @@ func (err ErrTopicNotExist) Unwrap() error {
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
return len(topic) <= 35 && topicPattern().MatchString(topic)
}
// SanitizeAndValidateTopics sanitizes and checks an array or topics

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"strings"
"sync"
"gitea.dev/actionslib/pkg/model"
@@ -75,7 +76,9 @@ func ParseWorkflowCallConfig(content []byte) (*model.WorkflowCall, error) {
const SecretsInherit = "inherit"
// callerSecretValueRegexp matches the `${{ secrets.NAME }}` form expected for each value in a caller's `secrets:` mapping.
var callerSecretValueRegexp = regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
var callerSecretValueRegexp = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^\s*\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}\s*$`)
})
// ParseCallerSecrets decodes a caller's "secrets:" YAML node into one of two forms:
// - inherit == true: the caller wrote `secrets: inherit`; mapping is nil
@@ -101,7 +104,7 @@ func ParseCallerSecrets(node yaml.Node) (inherit bool, mapping map[string]string
if err := v.Decode(&sv); err != nil {
return false, nil, fmt.Errorf("decode secret %q: %w", k.Value, err)
}
matches := callerSecretValueRegexp.FindStringSubmatch(sv)
matches := callerSecretValueRegexp().FindStringSubmatch(sv)
if len(matches) != 2 {
return false, nil, fmt.Errorf("caller secret %q value must be of the form ${{ secrets.NAME }}", k.Value)
}

View File

@@ -10,6 +10,7 @@ import (
"path"
"regexp"
"strings"
"sync"
"gitea.dev/modules/markup"
"gitea.dev/modules/translation"
@@ -71,15 +72,24 @@ func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
return delimiter
}
// quoteRegexp follows the RFC-4180 CSV standard for when double-quotes are used to enclose fields, then a double-quote appearing inside a
// field must be escaped by preceding it with another double quote. https://www.ietf.org/rfc/rfc4180.txt
// This finds all quoted strings that have escaped quotes.
var quoteRegexp = regexp.MustCompile(`"[^"]*"`)
var globalVars = sync.OnceValue(func() (ret struct {
quoteRegexp *regexp.Regexp
beforeAfterQuotes *regexp.Regexp
},
) {
// quoteRegexp follows the RFC-4180 CSV standard for when double-quotes are used to enclose fields, then a double-quote appearing inside a
// field must be escaped by preceding it with another double quote. https://www.ietf.org/rfc/rfc4180.txt
// This finds all quoted strings that have escaped quotes.
ret.quoteRegexp = regexp.MustCompile(`"[^"]*"`)
// Looks for possible delimiters right before or after (with spaces after the former) double quotes with closing quotes
ret.beforeAfterQuotes = regexp.MustCompile(`([,@\t;|]{0,1}) *(?:"[^"]*")+([,@\t;|]{0,1})`)
return ret
})
// removeQuotedStrings uses the quoteRegexp to remove all quoted strings so that we can reliably have each row on one line
// (quoted strings often have new lines within the string)
func removeQuotedString(text string) string {
return quoteRegexp.ReplaceAllLiteralString(text, "")
return globalVars().quoteRegexp.ReplaceAllLiteralString(text, "")
}
// guessDelimiter takes up to maxLines of the CSV text, iterates through the possible delimiters, and sees if the CSV Reader reads it without throwing any errors.
@@ -133,13 +143,10 @@ func FormatError(err error, locale translation.Locale) (string, error) {
return "", err
}
// Looks for possible delimiters right before or after (with spaces after the former) double quotes with closing quotes
var beforeAfterQuotes = regexp.MustCompile(`([,@\t;|]{0,1}) *(?:"[^"]*")+([,@\t;|]{0,1})`)
// guessFromBeforeAfterQuotes guesses the limiter by finding a double quote that has a valid delimiter before it and a closing quote,
// or a double quote with a closing quote and a valid delimiter after it
func guessFromBeforeAfterQuotes(data []byte) rune {
rs := beforeAfterQuotes.FindStringSubmatch(string(data)) // returns first match, or nil if none
rs := globalVars().beforeAfterQuotes.FindStringSubmatch(string(data)) // returns first match, or nil if none
if rs != nil {
if rs[1] != "" {
return rune(rs[1][0]) // delimiter found left of quoted string

View File

@@ -74,20 +74,43 @@ func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues {
return c.trailerValues
}
var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp {
var globalVars = sync.OnceValue(func() (ret struct {
commitMessageTrailerSplit *regexp.Regexp
hunkRegex *regexp.Regexp
refNamePatternInvalid *regexp.Regexp
patchCommits *regexp.Regexp
},
) {
// ref: https://git-scm.com/docs/git-interpret-trailers
// TODO: the regexp is not able to perfectly parse the all kinds of trailers
// It was just copied from legacy code, it is not exactly the same as how Git parses the trailer and not quite right in some cases.
// For the key characters: it follows RFC 822 field name syntax (or RFC 2822/RFC 5322): printable ASCII characters between 33 and 126 except the colon (:),
// but maybe we don't want to make it that complicated, so here we only support some common "symbol-like" characters.
return regexp.MustCompile(`(?s)^(?P<content>.*?)(?P<sep>^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P<trailer>(?:[A-Za-z0-9][-\w]*:[^\n]*(\n\s+[^\n]*)*\n?)*\n*)$`)
ret.commitMessageTrailerSplit = regexp.MustCompile(`(?s)^(?P<content>.*?)(?P<sep>^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P<trailer>(?:[A-Za-z0-9][-\w]*:[^\n]*(\n\s+[^\n]*)*\n?)*\n*)$`)
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
ret.hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`)
// refNamePatternInvalid is regular expression with unallowed characters in git reference name
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
ret.refNamePatternInvalid = regexp.MustCompile(
`[\000-\037\177 \\~^:?*[]|` + // No absolutely invalid characters
`(?:^[/.])|` + // Not HasPrefix("/") or "."
`(?:/\.)|` + // no "/."
`(?:\.lock$)|(?:\.lock/)|` + // No ".lock/"" or ".lock" at the end
`(?:\.\.)|` + // no ".." anywhere
`(?://)|` + // no "//" anywhere
`(?:@{)|` + // no "@{"
`(?:[/.]$)|` + // no terminal '/' or '.'
`(?:^@$)`) // Not "@"
ret.patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
return ret
})
// CommitMessageSplitTrailer tries to split the message by the trailer separator
// content + sep + trailer will reconstruct the original message
func CommitMessageSplitTrailer(s string) (content, sep, trailer string) {
s = util.NormalizeStringEOL(s)
re := commitMessageTrailerSplit()
re := globalVars().commitMessageTrailerSplit
v := re.FindStringSubmatch(s)
if v == nil {
return s, "", ""

View File

@@ -9,7 +9,6 @@ import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
@@ -132,9 +131,6 @@ func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightH
return leftLine, leftHunk, rightLine, rightHunk
}
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`)
const cmdDiffHead = "diff --git "
func isHeader(lof string, inHunk bool) bool {
@@ -192,9 +188,9 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
break
}
// A map with named groups of our regex to recognize them later more easily
submatches := hunkRegex.FindStringSubmatch(lof)
submatches := globalVars().hunkRegex.FindStringSubmatch(lof)
groups := make(map[string]string)
for i, name := range hunkRegex.SubexpNames() {
for i, name := range globalVars().hunkRegex.SubexpNames() {
if i != 0 && name != "" {
groups[name] = submatches[i]
}

View File

@@ -5,7 +5,6 @@ package git
import (
"context"
"regexp"
"strconv"
"strings"
@@ -22,27 +21,13 @@ const (
pullSuffix = "/head"
)
// refNamePatternInvalid is regular expression with unallowed characters in git reference name
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
var refNamePatternInvalid = regexp.MustCompile(
`[\000-\037\177 \\~^:?*[]|` + // No absolutely invalid characters
`(?:^[/.])|` + // Not HasPrefix("/") or "."
`(?:/\.)|` + // no "/."
`(?:\.lock$)|(?:\.lock/)|` + // No ".lock/"" or ".lock" at the end
`(?:\.\.)|` + // no ".." anywhere
`(?://)|` + // no "//" anywhere
`(?:@{)|` + // no "@{"
`(?:[/.]$)|` + // no terminal '/' or '.'
`(?:^@$)`) // Not "@"
// IsValidRefPattern ensures that the provided string could be a valid reference
func IsValidRefPattern(name string) bool {
return !refNamePatternInvalid.MatchString(name)
return !globalVars().refNamePatternInvalid.MatchString(name)
}
func SanitizeRefPattern(name string) string {
return refNamePatternInvalid.ReplaceAllString(name, "_")
return globalVars().refNamePatternInvalid.ReplaceAllString(name, "_")
}
// Reference represents a Git ref.

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"regexp"
"strings"
"gitea.dev/modules/git/gitcmd"
@@ -51,8 +50,6 @@ func (repo *Repository) GetDiffNumChangedFiles(ctx context.Context, base, head s
return w.numLines, nil
}
var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
// GetDiff generates and returns patch data between given revisions, optimized for human readability
func (repo *Repository) GetDiff(ctx context.Context, compareArg string, w io.Writer) error {
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
@@ -119,7 +116,7 @@ func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error
scanner := bufio.NewScanner(loadPatch)
scanner.Scan()
// Parse the Patch stats, sometimes Migration returns a 404 for the patch file
commitSHAGroups := patchCommits.FindStringSubmatch(scanner.Text())
commitSHAGroups := globalVars().patchCommits.FindStringSubmatch(scanner.Text())
if len(commitSHAGroups) != 0 {
commitSHA = commitSHAGroups[1]
} else {

View File

@@ -12,6 +12,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"gitea.dev/modules/packages"
"gitea.dev/modules/util"
@@ -38,6 +39,18 @@ const (
maxPKGINFOSize = 1024 * 1024
)
var globalVars = sync.OnceValue(func() (ret struct {
namePattern *regexp.Regexp
versionPattern *regexp.Regexp
},
) {
// https://man.archlinux.org/man/PKGBUILD.5
ret.namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
// (epoch:pkgver-pkgrel)
ret.versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
return ret
})
var (
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format")
@@ -45,11 +58,6 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
// https://man.archlinux.org/man/PKGBUILD.5
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
// (epoch:pkgver-pkgrel)
versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
// caps on the accumulated package file list (vars so tests can lower them); far above
// any legitimate package, but low enough to stop metadata amplification
maxFileEntries = 100000
@@ -251,10 +259,10 @@ func ParsePackageInfo(r io.Reader) (*Package, error) {
return nil, err
}
if !namePattern.MatchString(p.Name) {
if !globalVars().namePattern.MatchString(p.Name) {
return nil, ErrInvalidName
}
if !versionPattern.MatchString(p.Version) {
if !globalVars().versionPattern.MatchString(p.Version) {
return nil, ErrInvalidVersion
}
if p.FileMetadata.Architecture == "" {

View File

@@ -127,11 +127,11 @@ func TestParsePackageInfo(t *testing.T) {
})
t.Run("Regexp", func(t *testing.T) {
assert.Regexp(t, versionPattern, "1.2_3~4+5")
assert.Regexp(t, versionPattern, "1:2_3~4+5")
assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
assert.NotRegexp(t, versionPattern, "1.0.0 -1")
assert.Regexp(t, globalVars().versionPattern, "1.2_3~4+5")
assert.Regexp(t, globalVars().versionPattern, "1:2_3~4+5")
assert.NotRegexp(t, globalVars().versionPattern, "a:1.0.0-1")
assert.NotRegexp(t, globalVars().versionPattern, "0.0.1/1-1")
assert.NotRegexp(t, globalVars().versionPattern, "1.0.0 -1")
})
t.Run("InvalidVersion", func(t *testing.T) {

View File

@@ -8,6 +8,7 @@ import (
"errors"
"io"
"regexp"
"sync"
"gitea.dev/modules/json"
"gitea.dev/modules/packages"
@@ -60,7 +61,9 @@ type Dependency struct {
Package *string `json:"package"`
}
var nameMatch = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9-_]{0,63}\z`)
var nameMatch = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9-_]{0,63}\z`)
})
// ParsePackage reads the metadata and content of a package
func ParsePackage(r io.Reader) (*Package, error) {
@@ -120,7 +123,7 @@ func parsePackage(r io.Reader) (*Package, error) {
return nil, err
}
if !nameMatch.MatchString(meta.Name) {
if !nameMatch().MatchString(meta.Name) {
return nil, ErrInvalidName
}

View File

@@ -9,6 +9,7 @@ import (
"io"
"regexp"
"strings"
"sync"
"gitea.dev/modules/json"
"gitea.dev/modules/packages"
@@ -21,13 +22,20 @@ const (
SettingPublicPem = "chef.public_pem"
)
var globalVars = sync.OnceValue(func() (ret struct {
namePattern *regexp.Regexp
versionPattern *regexp.Regexp
},
) {
ret.namePattern = regexp.MustCompile(`\A\S+\z`)
ret.versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`)
return ret
})
var (
ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing")
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
namePattern = regexp.MustCompile(`\A\S+\z`)
versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`)
)
// Package represents a Chef package
@@ -108,11 +116,11 @@ func ParseChefMetadata(r io.Reader) (*Package, error) {
return nil, err
}
if !namePattern.MatchString(cm.Name) {
if !globalVars().namePattern.MatchString(cm.Name) {
return nil, ErrInvalidName
}
if !versionPattern.MatchString(cm.Version) {
if !globalVars().versionPattern.MatchString(cm.Version) {
return nil, ErrInvalidVersion
}

View File

@@ -14,6 +14,7 @@ import (
"path"
"regexp"
"strings"
"sync"
"gitea.dev/modules/charset"
"gitea.dev/modules/json"
@@ -117,7 +118,9 @@ type Author struct {
Homepage string `json:"homepage,omitempty"`
}
var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
var nameMatch = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
})
type ReadSeekAt interface {
io.Reader
@@ -237,7 +240,7 @@ func ParsePackage(r ReadSeekAt, optVersion ...string) (*PackageInfo, error) {
return nil, err
}
if !nameMatch.MatchString(cj.Name) {
if !nameMatch().MatchString(cj.Name) {
return nil, ErrInvalidName
}

View File

@@ -4,20 +4,36 @@
package conan
import (
"fmt"
"io"
"regexp"
"strings"
"sync"
)
var (
patternAuthor = compilePattern("author")
patternHomepage = compilePattern("homepage")
patternURL = compilePattern("url")
patternLicense = compilePattern("license")
patternDescription = compilePattern("description")
patternTopics = regexp.MustCompile(`(?im)^\s*topics\s*=\s*\((.+)\)`)
patternTopicList = regexp.MustCompile(`\s*['"](.+?)['"]\s*,?`)
)
var globalVars = sync.OnceValue(func() (ret struct {
patternAuthor *regexp.Regexp
patternHomepage *regexp.Regexp
patternURL *regexp.Regexp
patternLicense *regexp.Regexp
patternDescription *regexp.Regexp
patternTopics *regexp.Regexp
patternTopicList *regexp.Regexp
namePattern *regexp.Regexp
revisionPattern *regexp.Regexp
},
) {
ret.patternAuthor = compilePattern("author")
ret.patternHomepage = compilePattern("homepage")
ret.patternURL = compilePattern("url")
ret.patternLicense = compilePattern("license")
ret.patternDescription = compilePattern("description")
ret.patternTopics = regexp.MustCompile(`(?im)^\s*topics\s*=\s*\((.+)\)`)
ret.patternTopicList = regexp.MustCompile(`\s*['"](.+?)['"]\s*,?`)
ret.namePattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%d,%d}$`, minChars-1, maxChars-1))
ret.revisionPattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9]{1,%d}$`, maxChars))
return ret
})
func compilePattern(name string) *regexp.Regexp {
return regexp.MustCompile(`(?im)^\s*` + name + `\s*=\s*['"\(](.+)['"\)]`)
@@ -31,29 +47,29 @@ func ParseConanfile(r io.Reader) (*Metadata, error) {
metadata := &Metadata{}
m := patternAuthor.FindSubmatch(buf)
m := globalVars().patternAuthor.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.Author = string(m[1])
}
m = patternHomepage.FindSubmatch(buf)
m = globalVars().patternHomepage.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.ProjectURL = string(m[1])
}
m = patternURL.FindSubmatch(buf)
m = globalVars().patternURL.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.RepositoryURL = string(m[1])
}
m = patternLicense.FindSubmatch(buf)
m = globalVars().patternLicense.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.License = strings.ReplaceAll(strings.ReplaceAll(string(m[1]), "'", ""), "\"", "")
}
m = patternDescription.FindSubmatch(buf)
m = globalVars().patternDescription.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
metadata.Description = string(m[1])
}
m = patternTopics.FindSubmatch(buf)
m = globalVars().patternTopics.FindSubmatch(buf)
if len(m) > 1 && len(m[1]) > 0 {
m2 := patternTopicList.FindAllSubmatch(m[1], -1)
m2 := globalVars().patternTopicList.FindAllSubmatch(m[1], -1)
if len(m2) > 0 {
metadata.Keywords = make([]string, 0, len(m2))
for _, g := range m2 {

View File

@@ -5,7 +5,6 @@ package conan
import (
"fmt"
"regexp"
"strings"
"gitea.dev/modules/log"
@@ -21,12 +20,7 @@ const (
DefaultRevision = "0"
)
var (
namePattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%d,%d}$`, minChars-1, maxChars-1))
revisionPattern = regexp.MustCompile(fmt.Sprintf(`^[a-zA-Z0-9]{1,%d}$`, maxChars))
ErrValidation = util.NewInvalidArgumentErrorf("could not validate one or more reference fields")
)
var ErrValidation = util.NewInvalidArgumentErrorf("could not validate one or more reference fields")
// RecipeReference represents a recipe <Name>/<Version>@<User>/<Channel>#<Revision>
type RecipeReference struct {
@@ -51,7 +45,7 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
return nil, ErrValidation
}
if !namePattern.MatchString(name) {
if !globalVars().namePattern.MatchString(name) {
return nil, ErrValidation
}
@@ -59,13 +53,13 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
if v == "" {
return nil, ErrValidation
}
if user != "" && !namePattern.MatchString(user) {
if user != "" && !globalVars().namePattern.MatchString(user) {
return nil, ErrValidation
}
if channel != "" && !namePattern.MatchString(channel) {
if channel != "" && !globalVars().namePattern.MatchString(channel) {
return nil, ErrValidation
}
if revision != "" && !revisionPattern.MatchString(revision) {
if revision != "" && !globalVars().revisionPattern.MatchString(revision) {
return nil, ErrValidation
}
@@ -124,10 +118,10 @@ func NewPackageReference(recipe *RecipeReference, reference, revision string) (*
if recipe == nil {
return nil, ErrValidation
}
if reference == "" || !revisionPattern.MatchString(reference) {
if reference == "" || !globalVars().revisionPattern.MatchString(reference) {
return nil, ErrValidation
}
if revision != "" && !revisionPattern.MatchString(revision) {
if revision != "" && !globalVars().revisionPattern.MatchString(revision) {
return nil, ErrValidation
}

View File

@@ -13,6 +13,7 @@ import (
"path"
"regexp"
"strings"
"sync"
"gitea.dev/modules/base"
"gitea.dev/modules/packages"
@@ -34,12 +35,19 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var (
fieldPattern = regexp.MustCompile(`\A\S+:`)
namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`)
authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
)
var globalVars = sync.OnceValue(func() (ret struct {
fieldPattern *regexp.Regexp
namePattern *regexp.Regexp
versionPattern *regexp.Regexp
authorReplacePattern *regexp.Regexp
},
) {
ret.fieldPattern = regexp.MustCompile(`\A\S+:`)
ret.namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
ret.versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`)
ret.authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
return ret
})
// Package represents a CRAN package
type Package struct {
@@ -162,7 +170,7 @@ func ParseDescription(r io.Reader) (*Package, error) {
if line == "" {
continue
}
if !fieldPattern.MatchString(line) {
if !globalVars().fieldPattern.MatchString(line) {
b.WriteRune(' ')
b.WriteString(line)
continue
@@ -202,12 +210,12 @@ func setField(p *Package, data string) error {
switch name {
case "Package":
if !namePattern.MatchString(value) {
if !globalVars().namePattern.MatchString(value) {
return ErrInvalidName
}
p.Name = value
case "Version":
if !versionPattern.MatchString(value) {
if !globalVars().versionPattern.MatchString(value) {
return ErrInvalidVersion
}
p.Version = value
@@ -220,7 +228,7 @@ func setField(p *Package, data string) error {
case "License":
p.Metadata.License = value
case "Author":
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
p.Metadata.Authors = splitAndTrim(globalVars().authorReplacePattern.ReplaceAllString(value, ""))
case "Depends":
p.Metadata.Depends = splitAndTrim(value)
case "Imports":

View File

@@ -12,6 +12,7 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
@@ -42,7 +43,9 @@ const (
PropertySymbolID = "nuget.symbol.id"
)
var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`)
var idmatch = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`)
})
const maxNuspecFileSize = 3 * 1024 * 1024
@@ -171,7 +174,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
return nil, err
}
if !idmatch.MatchString(p.Metadata.ID) {
if !idmatch().MatchString(p.Metadata.ID) {
return nil, ErrNuspecInvalidID
}

View File

@@ -9,6 +9,7 @@ import (
"io"
"regexp"
"strings"
"sync"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
@@ -24,7 +25,9 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
var namePattern = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)
var namePattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)
})
// https://github.com/dart-lang/pub-dev/blob/4d582302a8d10152a5cd6129f65bf4f4dbca239d/pkg/pub_package_reader/lib/pub_package_reader.dart#L143
const maxPubspecFileSize = 128 * 1024
@@ -118,7 +121,7 @@ func ParsePubspecMetadata(r io.Reader) (*Package, error) {
return nil, err
}
if !namePattern.MatchString(p.Name) {
if !namePattern().MatchString(p.Name) {
return nil, ErrInvalidName
}

View File

@@ -10,6 +10,7 @@ import (
"path"
"regexp"
"strings"
"sync"
"gitea.dev/modules/json"
"gitea.dev/modules/packages"
@@ -19,14 +20,21 @@ import (
"github.com/hashicorp/go-version"
)
var globalVars = sync.OnceValue(func() (ret struct {
manifestPattern *regexp.Regexp
toolsVersionPattern *regexp.Regexp
},
) {
ret.manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
ret.toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
return ret
})
var (
ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing")
ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large")
ErrManifestFilesTooLarge = util.NewInvalidArgumentErrorf("Package.swift files are too large")
ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
)
const (
@@ -133,7 +141,7 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
var manifestFiles []*zip.File
manifestDir, manifestDepth := "", 0
for _, file := range zr.File {
if strings.HasSuffix(file.Name, "/") || !manifestPattern.MatchString(path.Base(file.Name)) {
if strings.HasSuffix(file.Name, "/") || !globalVars().manifestPattern.MatchString(path.Base(file.Name)) {
continue
}
dir, depth := path.Dir(file.Name), strings.Count(file.Name, "/")
@@ -156,7 +164,7 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
}
for _, file := range manifestFiles {
manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
manifestMatch := globalVars().manifestPattern.FindStringSubmatch(path.Base(file.Name))
if file.UncompressedSize64 > maxManifestFileSize {
return nil, ErrManifestFileTooLarge
@@ -190,7 +198,7 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
Content: string(content),
}
toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
toolsMatch := globalVars().toolsVersionPattern.FindStringSubmatch(manifest.Content)
if len(toolsMatch) == 2 {
v, err := version.NewSemver(toolsMatch[1])
if err != nil {

View File

@@ -15,6 +15,7 @@ import (
"regexp"
"slices"
"strings"
"sync"
packages_module "gitea.dev/modules/packages"
@@ -31,20 +32,25 @@ var (
const maxReadmeSize = 1024 * 1024 // larger READMEs are not stored, they would be rendered on every page view
// same rules as Terraform's module source address parser (github.com/hashicorp/terraform-registry-address)
var (
nameRe = regexp.MustCompile(`\A[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?\z`)
systemRe = regexp.MustCompile(`\A[0-9a-z]{1,64}\z`)
)
var globalVars = sync.OnceValue(func() (ret struct {
nameRe *regexp.Regexp
systemRe *regexp.Regexp
},
) {
// same rules as Terraform's module source address parser (github.com/hashicorp/terraform-registry-address)
ret.nameRe = regexp.MustCompile(`\A[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?\z`)
ret.systemRe = regexp.MustCompile(`\A[0-9a-z]{1,64}\z`)
return ret
})
func ValidateAddress(namespace, name, provider string) error {
if !nameRe.MatchString(namespace) {
if !globalVars().nameRe.MatchString(namespace) {
return ErrInvalidNamespace
}
if !nameRe.MatchString(name) {
if !globalVars().nameRe.MatchString(name) {
return ErrInvalidName
}
if !systemRe.MatchString(provider) {
if !globalVars().systemRe.MatchString(provider) {
return ErrInvalidProvider
}
return nil

View File

@@ -20,10 +20,19 @@ import (
"gitea.dev/modules/util"
)
var (
var globalVars = sync.OnceValue(func() (ret struct {
validNamePattern *regexp.Regexp
mentionPattern *regexp.Regexp
issueNumericPattern *regexp.Regexp
issueAlphanumericPattern *regexp.Regexp
crossReferenceIssueNumericPattern *regexp.Regexp
crossReferenceCommitPattern *regexp.Regexp
timeLogPattern *regexp.Regexp
},
) {
// validNamePattern performs only the most basic validation for user or repository names
// Repository name should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.
validNamePattern = regexp.MustCompile(`^[a-z0-9_.-]+$`)
ret.validNamePattern = regexp.MustCompile(`^[a-z0-9_.-]+$`)
// NOTE: All below regex matching do not perform any extra validation.
// Thus a link is produced even if the linked entity does not exist.
@@ -31,22 +40,25 @@ var (
// TODO: fix invalid linking issue
// mentionPattern matches all mentions in the form of "@user" or "@org/team"
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[-\w][-.\w]*?|@[-\w][-.\w]*?/[-\w][-.\w]*?)(?:\s|$|[:,;.?!](\s|$)|'|\)|\])`)
ret.mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[-\w][-.\w]*?|@[-\w][-.\w]*?/[-\w][-.\w]*?)(?:\s|$|[:,;.?!](\s|$)|'|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
// TODO: REFERENCE-REGEXP-PATTERN: findValidReferenceIndex checks a non-ASCII trailing character
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\"|[^\x00-\x7f])([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$|[^\x00-\x7f])`)
ret.issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\"|[^\x00-\x7f])([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$|[^\x00-\x7f])`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`)
ret.issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`)
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. org/repo#12345
// TODO: REFERENCE-REGEXP-PATTERN: findValidReferenceIndex checks a non-ASCII trailing character
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|[^\x00-\x7f])([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$|[^\x00-\x7f])`)
ret.crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|[^\x00-\x7f])([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$|[^\x00-\x7f])`)
// crossReferenceCommitPattern matches a string that references a commit in a different repository
// e.g. go-gitea/gitea@d8a994ef, go-gitea/gitea@d8a994ef243349f321568f9e36d5c3f444b99cae (7-40 characters)
crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,64})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
ret.crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,64})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
// timeLogPattern matches string for time tracking
timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
ret.timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
return ret
})
var (
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
issueKeywordWindow int
issueKeywordsOnce sync.Once
@@ -225,7 +237,7 @@ func FindAllMentionsBytes(content []byte) []RefSpan {
ret := make([]RefSpan, 0, 5)
pos := 0
for {
match := mentionPattern.FindSubmatchIndex(content[pos:])
match := globalVars().mentionPattern.FindSubmatchIndex(content[pos:])
if match == nil {
break
}
@@ -238,7 +250,7 @@ func FindAllMentionsBytes(content []byte) []RefSpan {
// FindFirstMentionBytes matches the first mention in then given content
// and returns the location of the unvalidated username, including the @ prefix.
func FindFirstMentionBytes(content []byte) (bool, RefSpan) {
mention := mentionPattern.FindSubmatchIndex(content)
mention := globalVars().mentionPattern.FindSubmatchIndex(content)
if mention == nil {
return false, RefSpan{}
}
@@ -365,10 +377,10 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool
contentBytes := util.UnsafeStringToBytes(content)
var match []int
if !crossLinkOnly {
match = findValidReferenceIndex(issueNumericPattern, contentBytes, 0)
match = findValidReferenceIndex(globalVars().issueNumericPattern, contentBytes, 0)
}
if match == nil {
if match = findValidReferenceIndex(crossReferenceIssueNumericPattern, contentBytes, 0); match == nil {
if match = findValidReferenceIndex(globalVars().crossReferenceIssueNumericPattern, contentBytes, 0); match == nil {
return nil
}
}
@@ -390,7 +402,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool
// FindRenderizableCommitCrossReference returns the first unvalidated commit cross reference found in a string.
func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableReference) {
m := crossReferenceCommitPattern.FindStringSubmatchIndex(content)
m := globalVars().crossReferenceCommitPattern.FindStringSubmatchIndex(content)
if len(m) < 8 {
return false, nil
}
@@ -436,7 +448,7 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *Re
// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference {
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
match := globalVars().issueAlphanumericPattern.FindStringSubmatchIndex(content)
if match == nil {
return nil
}
@@ -460,7 +472,7 @@ func findAllIssueReferencesBytes(content []byte, links []string, originalContent
// trailing spaces (\s#ref,\s), so if we get two consecutive references, the space
// from the second reference will be "eaten" by the first one:
// ...\s#ref1\s#ref2\s... --> ...`\s#ref1\s`, (not) `#ref2,\s...`
for _, pattern := range []*regexp.Regexp{issueNumericPattern, crossReferenceIssueNumericPattern} {
for _, pattern := range []*regexp.Regexp{globalVars().issueNumericPattern, globalVars().crossReferenceIssueNumericPattern} {
for match := findValidReferenceIndex(pattern, content, 0); match != nil; match = findValidReferenceIndex(pattern, content, match[3]) {
if ref := getCrossReference(content, match[2], match[3], false, false); ref != nil {
ret = append(ret, ref)
@@ -523,7 +535,7 @@ func findAllIssueReferencesBytes(content []byte, links []string, originalContent
pos := 0
for {
match := timeLogPattern.FindSubmatchIndex(content[pos:])
match := globalVars().timeLogPattern.FindSubmatchIndex(content[pos:])
if match == nil {
break
}
@@ -586,7 +598,7 @@ func getCrossReference(content []byte, start, end int, fromLink, prOnly bool) *r
return nil
}
owner, name := parts[0], parts[1]
if !validNamePattern.MatchString(owner) || !validNamePattern.MatchString(name) {
if !globalVars().validNamePattern.MatchString(owner) || !globalVars().validNamePattern.MatchString(name) {
return nil
}
action, location := findActionKeywords(content, start)

View File

@@ -483,11 +483,11 @@ func TestRegExp_mentionPattern(t *testing.T) {
}
for _, testCase := range trueTestCases {
found := mentionPattern.FindStringSubmatch(testCase.pat)
found := globalVars().mentionPattern.FindStringSubmatch(testCase.pat)
assert.Equal(t, testCase.exp, found[1])
}
for _, testCase := range falseTestCases {
res := mentionPattern.MatchString(testCase)
res := globalVars().mentionPattern.MatchString(testCase)
assert.False(t, res, "[%s] should be false", testCase)
}
}
@@ -515,10 +515,10 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
}
for _, testCase := range trueTestCases {
assert.True(t, issueNumericPattern.MatchString(testCase))
assert.True(t, globalVars().issueNumericPattern.MatchString(testCase))
}
for _, testCase := range falseTestCases {
assert.False(t, issueNumericPattern.MatchString(testCase))
assert.False(t, globalVars().issueNumericPattern.MatchString(testCase))
}
}
@@ -552,10 +552,10 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
}
for _, testCase := range trueTestCases {
assert.True(t, issueAlphanumericPattern.MatchString(testCase))
assert.True(t, globalVars().issueAlphanumericPattern.MatchString(testCase))
}
for _, testCase := range falseTestCases {
assert.False(t, issueAlphanumericPattern.MatchString(testCase))
assert.False(t, globalVars().issueAlphanumericPattern.MatchString(testCase))
}
}

View File

@@ -9,6 +9,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"gitea.dev/modules/log"
)
@@ -20,7 +21,15 @@ const (
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
var escapeRegex = regexp.MustCompile(escapeRegexpString)
var globalVars = sync.OnceValue(func() (ret struct {
escapeRegex *regexp.Regexp
extensionReg *regexp.Regexp
},
) {
ret.escapeRegex = regexp.MustCompile(escapeRegexpString)
ret.extensionReg = regexp.MustCompile(`^(\.[-\w]+)+$`)
return ret
})
func CollectEnvConfigKeys() (keys []string) {
for _, env := range os.Environ() {
@@ -47,7 +56,7 @@ func ClearEnvConfigKeys() {
func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
inKey := false
last := 0
escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1)
escapeStringIndices := globalVars().escapeRegex.FindAllStringIndex(encoded, -1)
for _, unescapeIdx := range escapeStringIndices {
preceding := encoded[last:unescapeIdx[0]]
if !inKey {

View File

@@ -6,7 +6,6 @@ package setting
import (
"regexp"
"strings"
"sync"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
@@ -222,14 +221,10 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR
return rule, true
}
var extensionReg = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^(\.[-\w]+)+$`)
})
func fileExtensionsToPatterns(sectionName string, extensions []string) []string {
patterns := make([]string, 0, len(extensions))
for _, extension := range extensions {
if !extensionReg().MatchString(extension) {
if !globalVars().extensionReg.MatchString(extension) {
log.Warn("Config section %s file extension %s is invalid. Extension ignored", sectionName, extension)
} else {
patterns = append(patterns, "*"+extension)

View File

@@ -29,7 +29,7 @@ func TestSubjectBodySeparator(t *testing.T) {
{"Multiple\n---\n-------\n---\nSeparators", "Multiple\n", "\n-------\n---\nSeparators"},
{"Insufficient\n--\nSeparators", "", "Insufficient\n--\nSeparators"},
} {
loc := mailSubjectSplit.FindStringIndex(testCase.input)
loc := globalVars().mailSubjectSplit.FindStringIndex(testCase.input)
if loc == nil {
assert.Empty(t, testCase.subject, "no subject found, but one expected")
assert.Equal(t, testCase.body, testCase.input)

View File

@@ -14,6 +14,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
texttemplate "text/template"
@@ -81,10 +82,31 @@ type templateErrorPrettier struct {
assets *assetfs.LayeredFS
}
var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
var globalVars = sync.OnceValue(func() (ret struct {
reGenericTemplateError *regexp.Regexp
reFuncNotDefinedError *regexp.Regexp
reUnexpectedOperandError *regexp.Regexp
reExpectedEndError *regexp.Regexp
reTemplateExecutingError *regexp.Regexp
reTemplateExecutingErrorMsg *regexp.Regexp
mailSubjectSplit *regexp.Regexp
codeMatcher *regexp.Regexp
},
) {
ret.reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
ret.reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
ret.reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
ret.reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
ret.reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
ret.reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
ret.mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
// Match text that is between back ticks.
ret.codeMatcher = regexp.MustCompile("`([^`]+)`")
return ret
})
func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
groups := reGenericTemplateError.FindStringSubmatch(err.Error())
groups := globalVars().reGenericTemplateError.FindStringSubmatch(err.Error())
if len(groups) != 4 {
return ""
}
@@ -92,10 +114,8 @@ func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
return p.makeDetailedError(message, tmplName, lineStr, "", "")
}
var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
groups := reFuncNotDefinedError.FindStringSubmatch(err.Error())
groups := globalVars().reFuncNotDefinedError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
}
@@ -104,10 +124,8 @@ func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
return p.makeDetailedError(message, tmplName, lineStr, "", funcName)
}
var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
groups := globalVars().reUnexpectedOperandError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
}
@@ -116,10 +134,8 @@ func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
return p.makeDetailedError(message, tmplName, lineStr, "", unexpected)
}
var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
groups := reExpectedEndError.FindStringSubmatch(err.Error())
groups := globalVars().reExpectedEndError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
}
@@ -127,16 +143,11 @@ func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
return p.makeDetailedError(message, tmplName, lineStr, "", unexpected)
}
var (
reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
)
func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
if groups := globalVars().reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
tmplName, lineStr, posStr, msgPart := groups[1], groups[2], groups[3], groups[4]
target := ""
if groups = reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
if groups = globalVars().reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
target = groups[2]
}
return p.makeDetailedError(msgPart, tmplName, lineStr, posStr, target)

View File

@@ -8,7 +8,6 @@ import (
"html/template"
"io"
"net/url"
"regexp"
"slices"
"strings"
"sync"
@@ -104,8 +103,6 @@ func mailBodyFuncMap() template.FuncMap {
}
}
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
func newMailRenderer() (*MailRender, error) {
subjectTemplates := texttmpl.New("")
subjectTemplates.Funcs(mailSubjectTextFuncMap())
@@ -155,7 +152,7 @@ func newMailRenderer() (*MailRender, error) {
}
var subjectContent []byte
bodyContent := content
loc := mailSubjectSplit.FindIndex(content)
loc := globalVars().mailSubjectSplit.FindIndex(content)
if loc != nil {
subjectContent, bodyContent = content[0:loc[0]], content[loc[1]:]
}

View File

@@ -9,7 +9,6 @@ import (
"html/template"
"math"
"net/url"
"regexp"
"slices"
"strings"
@@ -78,12 +77,9 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ
return renderedMessage
}
// Match text that is between back ticks.
var codeMatcher = regexp.MustCompile("`([^`]+)`")
// renderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags
htmlWithCodeTags := globalVars().codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags
return template.HTML(htmlWithCodeTags)
}

View File

@@ -22,6 +22,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"time"
user_model "gitea.dev/models/user"
@@ -34,13 +35,19 @@ const (
maxTimeDifference = 10 * time.Minute
)
var (
algorithmPattern = regexp.MustCompile(`algorithm=(\w+)`)
versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`)
authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`)
var globalVars = sync.OnceValue(func() (ret struct {
algorithmPattern *regexp.Regexp
versionPattern *regexp.Regexp
authorizationPattern *regexp.Regexp
},
) {
ret.algorithmPattern = regexp.MustCompile(`algorithm=(\w+)`)
ret.versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`)
ret.authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`)
return ret
})
_ auth.Method = &Auth{}
)
var _ auth.Method = &Auth{}
// Documentation:
// https://docs.chef.io/server/api_chef_server/#required-headers
@@ -139,7 +146,7 @@ func getSignVersion(req *http.Request) (string, error) {
return "", util.NewInvalidArgumentErrorf("X-Ops-Sign header missing")
}
m := versionPattern.FindStringSubmatch(hdr)
m := globalVars().versionPattern.FindStringSubmatch(hdr)
if len(m) != 2 {
return "", util.NewInvalidArgumentErrorf("invalid X-Ops-Sign header")
}
@@ -152,7 +159,7 @@ func getSignVersion(req *http.Request) (string, error) {
version := m[1]
m = algorithmPattern.FindStringSubmatch(hdr)
m = globalVars().algorithmPattern.FindStringSubmatch(hdr)
if len(m) == 2 && m[1] != "sha1" && !(m[1] == "sha256" && version == "1.3") {
return "", util.NewInvalidArgumentErrorf("unsupported algorithm")
}
@@ -181,7 +188,7 @@ func verifySignedHeaders(req *http.Request, version string, pub *rsa.PublicKey)
func getAuthorizationData(req *http.Request) ([]byte, error) {
valueList := make(map[int]string)
for k, vs := range req.Header {
if m := authorizationPattern.FindStringSubmatch(k); m != nil {
if m := globalVars().authorizationPattern.FindStringSubmatch(k); m != nil {
index, _ := strconv.Atoi(m[1])
var v string
if len(vs) == 0 {

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"unicode"
packages_model "gitea.dev/models/packages"
@@ -17,10 +18,15 @@ import (
packages_service "gitea.dev/services/packages"
)
var (
packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
)
var globalVars = sync.OnceValue(func() (ret struct {
packageNameRegex *regexp.Regexp
filenameRegex *regexp.Regexp
},
) {
ret.packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
ret.filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
return ret
})
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
@@ -54,11 +60,11 @@ func isValidPackageName(packageName string) bool {
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
return false
}
return packageNameRegex.MatchString(packageName) && packageName != ".."
return globalVars().packageNameRegex.MatchString(packageName) && packageName != ".."
}
func isValidFileName(filename string) bool {
return filenameRegex.MatchString(filename) &&
return globalVars().filenameRegex.MatchString(filename) &&
strings.TrimSpace(filename) == filename &&
filename != "." && filename != ".."
}

View File

@@ -18,6 +18,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/globallock"
@@ -46,7 +47,9 @@ const (
var (
errInvalidParameters = errors.New("request parameters are invalid")
illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`)
illegalCharacters = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`[\\/:"<>|?*]`)
})
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -471,7 +474,7 @@ func extractPathParameters(ctx *context.Context) (parameters, error) {
parts = parts[:len(parts)-1]
}
if illegalCharacters.MatchString(p.Version) {
if illegalCharacters().MatchString(p.Version) {
return p, errInvalidParameters
}
@@ -482,7 +485,7 @@ func extractPathParameters(ctx *context.Context) (parameters, error) {
p.ArtifactID = parts[len(parts)-1]
p.GroupID = strings.Join(parts[:len(parts)-1], ".")
if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) {
if illegalCharacters().MatchString(p.GroupID) || illegalCharacters().MatchString(p.ArtifactID) {
return p, errInvalidParameters
}

View File

@@ -13,6 +13,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
@@ -90,10 +91,15 @@ func FeedCapabilityResource(ctx *context.Context) {
xmlResponse(ctx, http.StatusOK, Metadata)
}
var (
searchTermExtract = regexp.MustCompile(`'([^']+)'`)
searchTermExact = regexp.MustCompile(`\s+eq\s+'`)
)
var globalVars = sync.OnceValue(func() (ret struct {
searchTermExtract *regexp.Regexp
searchTermExact *regexp.Regexp
},
) {
ret.searchTermExtract = regexp.MustCompile(`'([^']+)'`)
ret.searchTermExact = regexp.MustCompile(`\s+eq\s+'`)
return ret
})
func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
@@ -109,11 +115,11 @@ func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
// https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ section 4.5
// We don't support these queries, just extract the search term.
filter := ctx.FormTrim("$filter")
match := searchTermExtract.FindStringSubmatch(filter)
match := globalVars().searchTermExtract.FindStringSubmatch(filter)
if len(match) == 2 {
return packages_model.SearchValue{
Value: strings.TrimSpace(match[1]),
ExactMatch: searchTermExact.MatchString(filter),
ExactMatch: globalVars().searchTermExact.MatchString(filter),
}
}

View File

@@ -10,6 +10,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"unicode"
packages_model "gitea.dev/models/packages"
@@ -23,13 +24,14 @@ import (
)
// https://peps.python.org/pep-0426/#name
var (
normalizer = strings.NewReplacer(".", "-", "_", "-")
nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
)
// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
var versionMatcher = regexp.MustCompile(`\Av?` +
var globalVars = sync.OnceValue(func() (ret struct {
nameMatcher *regexp.Regexp
versionMatcher *regexp.Regexp
},
) {
ret.nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
ret.versionMatcher = regexp.MustCompile(`\Av?` +
`(?:[0-9]+!)?` + // epoch
`[0-9]+(?:\.[0-9]+)*` + // release segment
`(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release
@@ -37,6 +39,10 @@ var versionMatcher = regexp.MustCompile(`\Av?` +
`(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release
`(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version
`\z`)
return ret
})
var normalizer = strings.NewReplacer(".", "-", "_", "-")
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
@@ -225,5 +231,5 @@ func normalizeLabel(label string) string {
}
func isValidNameAndVersion(packageName, packageVersion string) bool {
return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
return globalVars().nameMatcher.MatchString(packageName) && globalVars().versionMatcher.MatchString(packageVersion)
}

View File

@@ -12,6 +12,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
@@ -35,12 +36,17 @@ const (
AcceptZip = "application/vnd.swift.registry.v1+zip"
)
var (
var globalVars = sync.OnceValue(func() (ret struct {
scopePattern *regexp.Regexp
namePattern *regexp.Regexp
},
) {
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#361-package-scope
scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
ret.scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#362-package-name
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
)
ret.namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
return ret
})
type headers struct {
Status int
@@ -327,7 +333,7 @@ func UploadPackageFile(ctx *context.Context) {
_, err := version.NewSemver(packageVersion)
if err != nil || !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) {
if err != nil || !globalVars().scopePattern.MatchString(packageScope) || !globalVars().namePattern.MatchString(packageName) {
apiError(ctx, http.StatusBadRequest, err)
return
}

View File

@@ -11,6 +11,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"unicode"
packages_model "gitea.dev/models/packages"
@@ -24,7 +25,9 @@ import (
packages_service "gitea.dev/services/packages"
)
var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
var packageNameRegex = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`\A[-_+.\w]+\z`)
})
const (
stateFilename = "tfstate"
@@ -81,7 +84,7 @@ func isValidPackageName(packageName string) bool {
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
return false
}
return packageNameRegex.MatchString(packageName) && packageName != ".."
return packageNameRegex().MatchString(packageName) && packageName != ".."
}
// UploadState uploads the specific terraform package.

View File

@@ -10,6 +10,7 @@ import (
"net/url"
"regexp"
"strconv"
"sync"
"gitea.dev/models/auth"
"gitea.dev/models/db"
@@ -35,10 +36,15 @@ const (
tplAuthEdit templates.TplName = "admin/auth/edit"
)
var (
separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
)
var globalVars = sync.OnceValue(func() (ret struct {
separatorAntiPattern *regexp.Regexp
langCodePattern *regexp.Regexp
},
) {
ret.separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
ret.langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
return ret
})
// Authentications show authentication config page
func Authentications(ctx *context.Context) {
@@ -204,12 +210,12 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.require_error", ctx.Locale.TrString("form.SSPISeparatorReplacement")))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
if globalVars().separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.alpha_dash_dot_error", ctx.Locale.TrString("form.SSPISeparatorReplacement")))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
if form.SSPIDefaultLanguage != "" && !globalVars().langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
}

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
@@ -19,7 +20,9 @@ import (
"go.yaml.in/yaml/v4"
)
var mailDarkSchemeQuery = regexp.MustCompile(`@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)`)
var mailDarkSchemeQuery = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)`)
})
func mailPreviewMockData(tmplName string) (map[string]any, error) {
mockData := map[string]any{}
@@ -51,7 +54,7 @@ func MailPreviewRender(ctx *context.Context) {
body = strings.ReplaceAll(body, `src="cid:`, `src="`+setting.AppSubURL+`/devtest/mail-preview-embed/`)
// a page can force "color-scheme" on an embedded document but never "prefers-color-scheme"
if scheme := ctx.FormString("scheme"); scheme == "light" || scheme == "dark" {
body = mailDarkSchemeQuery.ReplaceAllString(body, util.Iif(scheme == "dark", "@media all", "@media not all"))
body = mailDarkSchemeQuery().ReplaceAllString(body, util.Iif(scheme == "dark", "@media all", "@media not all"))
body = strings.Replace(body, "</head>", "<style>:root {color-scheme: "+scheme+"}</style></head>", 1)
}
// fragment templates like "mail/base/head" would be sniffed as text/plain otherwise

View File

@@ -13,6 +13,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
activities_model "gitea.dev/models/activities"
asymkey_model "gitea.dev/models/asymkey"
@@ -183,7 +184,7 @@ func Milestones(ctx *context.Context) {
}
if len(reposQuery) != 0 {
if issueReposQueryPattern.MatchString(reposQuery) {
if issueReposQueryPattern().MatchString(reposQuery) {
// remove "[" and "]" from string
reposQuery = reposQuery[1 : len(reposQuery)-1]
// for each ID (delimiter ",") add to int to repoIDs
@@ -353,7 +354,9 @@ func Issues(ctx *context.Context) {
}
// Regexp for repos query
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
var issueReposQueryPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
})
func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// ----------------------------------------------------

View File

@@ -10,6 +10,7 @@ import (
"path"
"regexp"
"strings"
"sync"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
@@ -38,7 +39,9 @@ func (err ErrFileTypeForbidden) Unwrap() error {
return util.ErrInvalidArgument
}
var wildcardTypeRe = regexp.MustCompile(`^[a-z]+/\*$`)
var wildcardTypeRe = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^[a-z]+/\*$`)
})
// Verify validates whether a file is allowed to be uploaded. If buf is empty, it will just check if the file
// has an allowed file extension.
@@ -80,7 +83,7 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error {
if mimeType == allowEntry {
return nil // mime type is allowed
}
if wildcardTypeRe.MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) {
if wildcardTypeRe().MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) {
return nil // wildcard match, e.g. image/*
}
}

View File

@@ -13,6 +13,7 @@ import (
"slices"
"strconv"
"strings"
"sync"
"time"
issues_model "gitea.dev/models/issues"
@@ -35,11 +36,13 @@ const (
secondsByMonth = 4 * secondsByWeek // seconds in a month
)
var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
var reDuration = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
})
// timeLogToAmount parses time log string and returns amount in seconds
func timeLogToAmount(str string) int64 {
matches := reDuration.FindAllStringSubmatch(str, -1)
matches := reDuration().FindAllStringSubmatch(str, -1)
if len(matches) == 0 {
return 0
}

View File

@@ -17,6 +17,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
auth_model "gitea.dev/models/auth"
@@ -111,7 +112,9 @@ func CheckAcceptMediaType(ctx *context.Context) {
}
}
var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)-(\d*).*`)
var rangeHeaderRegexp = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`bytes=(\d+)-(\d*).*`)
})
// DownloadHandler gets the content from the content store
func DownloadHandler(ctx *context.Context) {
@@ -128,7 +131,7 @@ func DownloadHandler(ctx *context.Context) {
toByte = meta.Size - 1
statusCode := http.StatusOK
if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr)
match := rangeHeaderRegexp().FindStringSubmatch(rangeHdr)
if len(match) > 1 {
statusCode = http.StatusPartialContent
fromByte, _ = strconv.ParseInt(match[1], 10, 32)

View File

@@ -15,6 +15,7 @@ import (
"mime"
"regexp"
"strings"
"sync"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
@@ -31,7 +32,9 @@ import (
const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
var subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
var subjectRemoveSpaces = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`[\s]+`)
})
func LoadedTemplates() *templates.MailRender {
return templates.MailRenderer()
@@ -47,7 +50,7 @@ func SendTestMail(email string) error {
}
func sanitizeSubject(subject string) string {
runes := []rune(strings.TrimSpace(subjectRemoveSpaces.ReplaceAllLiteralString(subject, " ")))
runes := []rune(strings.TrimSpace(subjectRemoveSpaces().ReplaceAllLiteralString(subject, " ")))
if len(runes) > mailMaxSubjectRunes {
runes = runes[:mailMaxSubjectRunes]
}

View File

@@ -14,6 +14,7 @@ import (
"regexp"
"slices"
"strings"
"sync"
"time"
issues_model "gitea.dev/models/issues"
@@ -564,7 +565,9 @@ func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Com
return allComments, true, nil
}
var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
var targetBranchChangeRegexp = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
})
func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
comment := &base.Comment{
@@ -580,7 +583,7 @@ func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.N
// Try to find the underlying event of system notes.
if note.System {
if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil {
if match := targetBranchChangeRegexp().FindStringSubmatch(note.Body); match != nil {
comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String()
comment.Meta["OldRef"] = match[1]
comment.Meta["NewRef"] = match[2]

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"regexp"
"sync"
"time"
audit_model "gitea.dev/models/audit"
@@ -28,7 +29,9 @@ import (
repo_service "gitea.dev/services/repository"
)
var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
var stripExitStatus = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`exit status \d+ - `)
})
// AddPushMirrorRemote registers the push mirror remote.
func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) (retErr error) {
@@ -132,7 +135,7 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
if err != nil {
err = util.SanitizeErrorCredentialURLs(err)
log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "")
m.LastError = stripExitStatus().ReplaceAllLiteralString(err.Error(), "")
}
m.LastUpdateUnix = timeutil.TimeStampNow()

View File

@@ -13,6 +13,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"unicode"
"uuid"
@@ -666,7 +667,9 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g
return nil
}
var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)
var escapedSymbols = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`([*[?! \\])`)
})
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {

View File

@@ -220,7 +220,7 @@ func getDiffTree(ctx context.Context, repo git.RepositoryFacade, baseBranch, hea
for scanner.Scan() {
treePath := scanner.Text()
// escape '*', '?', '[', spaces and '!' prefix
treePath = escapedSymbols.ReplaceAllString(treePath, `\$1`)
treePath = escapedSymbols().ReplaceAllString(treePath, `\$1`)
// no necessary to escape the first '#' symbol because the first symbol is '/'
if _, err := fmt.Fprintf(out, "/%s\n", treePath); err != nil {
return err

View File

@@ -13,6 +13,7 @@ import (
"net/url"
"regexp"
"strings"
"sync"
webhook_model "gitea.dev/models/webhook"
"gitea.dev/modules/base"
@@ -267,10 +268,12 @@ func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload,
return m.newPayload(text)
}
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
var urlRegex = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
})
func getMessageBody(htmlText string) string {
htmlText = urlRegex.ReplaceAllString(htmlText, "[$2]($1)")
htmlText = urlRegex().ReplaceAllString(htmlText, "[$2]($1)")
htmlText = strings.ReplaceAll(htmlText, "<br>", "\n")
return htmlText
}