From a165fdb63c8bd4c812fdbb82445ec02ca6d4b921 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 7 Nov 2026 11:56:56 +0000 Subject: [PATCH] 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. --- build/generate-emoji.go | 7 ++- build/generate-go-licenses.go | 29 ++++++++----- build/openapi3gen/convert.go | 21 ++++++--- build/openapi3gen/enumscan.go | 5 +-- models/issues/issue.go | 18 +++++--- models/project/column.go | 7 ++- models/repo/topic.go | 7 ++- modules/actions/jobparser/workflow_call.go | 7 ++- modules/csv/csv.go | 25 +++++++---- modules/git/commit_message.go | 29 +++++++++++-- modules/git/diff.go | 8 +--- modules/git/ref.go | 19 +------- modules/git/repo_compare.go | 5 +-- modules/packages/arch/metadata.go | 22 +++++++--- modules/packages/arch/metadata_test.go | 10 ++--- modules/packages/cargo/parser.go | 7 ++- modules/packages/chef/metadata.go | 18 +++++--- modules/packages/composer/metadata.go | 7 ++- modules/packages/conan/conanfile_parser.go | 48 ++++++++++++++------- modules/packages/conan/reference.go | 20 +++------ modules/packages/cran/metadata.go | 28 +++++++----- modules/packages/nuget/metadata.go | 7 ++- modules/packages/pub/metadata.go | 7 ++- modules/packages/swift/metadata.go | 20 ++++++--- modules/packages/terraform_module/parser.go | 22 ++++++---- modules/references/references.go | 46 ++++++++++++-------- modules/references/references_test.go | 12 +++--- modules/setting/config_env.go | 13 +++++- modules/setting/markup.go | 7 +-- modules/templates/helper_test.go | 2 +- modules/templates/htmlrenderer.go | 47 ++++++++++++-------- modules/templates/mail.go | 5 +-- modules/templates/util_render.go | 6 +-- routers/api/packages/chef/auth.go | 25 +++++++---- routers/api/packages/generic/generic.go | 18 +++++--- routers/api/packages/maven/maven.go | 9 ++-- routers/api/packages/nuget/nuget.go | 18 +++++--- routers/api/packages/pypi/pypi.go | 34 +++++++++------ routers/api/packages/swift/swift.go | 16 ++++--- routers/api/packages/terraform/terraform.go | 7 ++- routers/web/admin/auths.go | 18 +++++--- routers/web/devtest/mail_preview.go | 7 ++- routers/web/user/home.go | 7 ++- services/context/upload/upload.go | 7 ++- services/issue/commit.go | 7 ++- services/lfs/server.go | 7 ++- services/mailer/mail.go | 7 ++- services/migrations/gitlab.go | 7 ++- services/mirror/mirror_push.go | 7 ++- services/pull/merge.go | 5 ++- services/pull/merge_prepare.go | 2 +- services/webhook/matrix.go | 7 ++- 52 files changed, 474 insertions(+), 282 deletions(-) diff --git a/build/generate-emoji.go b/build/generate-emoji.go index dc3c1aa7362..c0081cc086a 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -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 { diff --git a/build/generate-go-licenses.go b/build/generate-go-licenses.go index a4aa5b08323..623d26bc43a 100644 --- a/build/generate-go-licenses.go +++ b/build/generate-go-licenses.go @@ -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) } } diff --git a/build/openapi3gen/convert.go b/build/openapi3gen/convert.go index d1ee1a3a68f..ae2370ace31 100644 --- a/build/openapi3gen/convert.go +++ b/build/openapi3gen/convert.go @@ -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 } } diff --git a/build/openapi3gen/enumscan.go b/build/openapi3gen/enumscan.go index 73bbcb5e150..199f2c9f00c 100644 --- a/build/openapi3gen/enumscan.go +++ b/build/openapi3gen/enumscan.go @@ -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 } diff --git a/models/issues/issue.go b/models/issues/issue.go index 3ed96964d10..ec124c90666 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -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. diff --git a/models/project/column.go b/models/project/column.go index 80432d54f2e..3f77281c7a9 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -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 diff --git a/models/repo/topic.go b/models/repo/topic.go index 8656d334187..84c28b03816 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -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 diff --git a/modules/actions/jobparser/workflow_call.go b/modules/actions/jobparser/workflow_call.go index 05a0a88888d..8e0d41c3fa3 100644 --- a/modules/actions/jobparser/workflow_call.go +++ b/modules/actions/jobparser/workflow_call.go @@ -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) } diff --git a/modules/csv/csv.go b/modules/csv/csv.go index 1985e27828c..8be1d772919 100644 --- a/modules/csv/csv.go +++ b/modules/csv/csv.go @@ -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 diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go index f7f9b923844..eb52f478c4f 100644 --- a/modules/git/commit_message.go +++ b/modules/git/commit_message.go @@ -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.*?)(?P^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P(?:[A-Za-z0-9][-\w]*:[^\n]*(\n\s+[^\n]*)*\n?)*\n*)$`) + ret.commitMessageTrailerSplit = regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P(?:[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[0-9]+)(,(?P[0-9]+))? \+(?P[0-9]+)(,(?P[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, "", "" diff --git a/modules/git/diff.go b/modules/git/diff.go index 5326d4024a8..6fe40c58f10 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -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[0-9]+)(,(?P[0-9]+))? \+(?P[0-9]+)(,(?P[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] } diff --git a/modules/git/ref.go b/modules/git/ref.go index cf7640003ad..581f3479eba 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -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. diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 93e634571ec..5a8734fc586 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -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 { diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index 886658f7594..af7c1cd9735 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -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 == "" { diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go index 821c0adc9ee..1d0b7f88cfd 100644 --- a/modules/packages/arch/metadata_test.go +++ b/modules/packages/arch/metadata_test.go @@ -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) { diff --git a/modules/packages/cargo/parser.go b/modules/packages/cargo/parser.go index 8900cffc2ee..a332bd53acc 100644 --- a/modules/packages/cargo/parser.go +++ b/modules/packages/cargo/parser.go @@ -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 } diff --git a/modules/packages/chef/metadata.go b/modules/packages/chef/metadata.go index e0db412458f..f9997c4d2d9 100644 --- a/modules/packages/chef/metadata.go +++ b/modules/packages/chef/metadata.go @@ -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 } diff --git a/modules/packages/composer/metadata.go b/modules/packages/composer/metadata.go index 27ad4222960..24174e362bf 100644 --- a/modules/packages/composer/metadata.go +++ b/modules/packages/composer/metadata.go @@ -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 } diff --git a/modules/packages/conan/conanfile_parser.go b/modules/packages/conan/conanfile_parser.go index c47b242cdc0..a767e1e9100 100644 --- a/modules/packages/conan/conanfile_parser.go +++ b/modules/packages/conan/conanfile_parser.go @@ -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 { diff --git a/modules/packages/conan/reference.go b/modules/packages/conan/reference.go index 3fe8f907745..d580553a279 100644 --- a/modules/packages/conan/reference.go +++ b/modules/packages/conan/reference.go @@ -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 /@/# 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 } diff --git a/modules/packages/cran/metadata.go b/modules/packages/cran/metadata.go index 83f075889fe..ed09a9986ef 100644 --- a/modules/packages/cran/metadata.go +++ b/modules/packages/cran/metadata.go @@ -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": diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index b73cb5d3ad9..80729c71b26 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -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 } diff --git a/modules/packages/pub/metadata.go b/modules/packages/pub/metadata.go index 7291836e1fa..e576f6aa6ed 100644 --- a/modules/packages/pub/metadata.go +++ b/modules/packages/pub/metadata.go @@ -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 } diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go index 8adaec915db..2396308568a 100644 --- a/modules/packages/swift/metadata.go +++ b/modules/packages/swift/metadata.go @@ -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 { diff --git a/modules/packages/terraform_module/parser.go b/modules/packages/terraform_module/parser.go index 612b52e5419..4aba1f355b8 100644 --- a/modules/packages/terraform_module/parser.go +++ b/modules/packages/terraform_module/parser.go @@ -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 diff --git a/modules/references/references.go b/modules/references/references.go index 46e89ab16c0..22a534f33a9 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -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) diff --git a/modules/references/references_test.go b/modules/references/references_test.go index bfa5220a0e7..05f356c7d02 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -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)) } } diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index 18c94262e3a..7d5a50a3f72 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -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 { diff --git a/modules/setting/markup.go b/modules/setting/markup.go index fa2d9367fd4..4760f5b07b8 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -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) diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index 36be81e8786..0c3386fda6f 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -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) diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 23b61751c11..47cf112b791 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -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) diff --git a/modules/templates/mail.go b/modules/templates/mail.go index 023be18f839..d29b148d8df 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -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]:] } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 42b195f7cb1..cf70c0bb1dd 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -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 "" block, intended for issue and PR titles func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { - htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `$1`) // replace with HTML tags + htmlWithCodeTags := globalVars().codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `$1`) // replace with HTML tags return template.HTML(htmlWithCodeTags) } diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index 3d3902bdc6a..467a4c8f10f 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -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 { diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index f1ad4efe9fe..94537d70495 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -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 != ".." } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 0b6d9085efd..0f792057ead 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -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 } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 8aa21e6a10a..596e22d2700 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -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), } } diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index c34e5902a4a..25558f58bb8 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -10,6 +10,7 @@ import ( "regexp" "sort" "strings" + "sync" "unicode" packages_model "gitea.dev/models/packages" @@ -23,20 +24,25 @@ 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`) -) +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 + `(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release + `(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release + `(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version + `\z`) + return ret +}) -// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions -var 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 - `(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release - `(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release - `(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version - `\z`) +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) } diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 997a3dbccaa..c392c059ba8 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -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 } diff --git a/routers/api/packages/terraform/terraform.go b/routers/api/packages/terraform/terraform.go index dbd115eee83..42dd2c5172e 100644 --- a/routers/api/packages/terraform/terraform.go +++ b/routers/api/packages/terraform/terraform.go @@ -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. diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 16ec38bc118..31c4045a33a 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -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")) } diff --git a/routers/web/devtest/mail_preview.go b/routers/web/devtest/mail_preview.go index 103eef16f37..de602941ccb 100644 --- a/routers/web/devtest/mail_preview.go +++ b/routers/web/devtest/mail_preview.go @@ -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, "", "", 1) } // fragment templates like "mail/base/head" would be sniffed as text/plain otherwise diff --git a/routers/web/user/home.go b/routers/web/user/home.go index aaabcf1f79b..ac0e0027374 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -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) { // ---------------------------------------------------- diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 0d69a85b92b..ee70c936da6 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -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/* } } diff --git a/services/issue/commit.go b/services/issue/commit.go index 1847979f7ce..d501807a2e9 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -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 } diff --git a/services/lfs/server.go b/services/lfs/server.go index 1b556d6b2d1..f093a940504 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -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) diff --git a/services/mailer/mail.go b/services/mailer/mail.go index ee4ba64d653..b7a5c0f5f09 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -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] } diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index 70d75bc6d40..92bd60a85f2 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -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] diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 517ac728578..381151d498e 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -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() diff --git a/services/pull/merge.go b/services/pull/merge.go index 21122fda42c..17690b01b1c 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -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) { diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 59b0e45297d..9e0453e6534 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -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 diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index dd7f5cb3636..6cf9cafd8f7 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -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(`]*?href="([^">]*?)">(.*?)`) +var urlRegex = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) +}) func getMessageBody(htmlText string) string { - htmlText = urlRegex.ReplaceAllString(htmlText, "[$2]($1)") + htmlText = urlRegex().ReplaceAllString(htmlText, "[$2]($1)") htmlText = strings.ReplaceAll(htmlText, "
", "\n") return htmlText }