mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
Use better string splitting techniques where possible (#8226)
## What changed? This PR aims to avoid usage of [`strings.Split`](https://pkg.go.dev/strings#Split) where possible in favor of better string splitting techniques, speficially: [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) where appropriate. There was also a [`strings.Fields`](https://pkg.go.dev/strings#Fields) change I made to use [`strings.FieldsSeq`](https://pkg.go.dev/strings#FieldsSeq) instead, and another for S3 to use the [`path`](https://pkg.go.dev/path) package instead of [`strings.Split`](https://pkg.go.dev/strings#Split). ## Why? [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) are often better options in many cases, and can be _partially_ detected using [`modernize`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize): > `stringsseq`: replace Split in "for range strings.Split(...)" by go1.24's more efficient `SplitSeq`, or `Fields` with `FieldSeq`. ## How did you test it? - [X] built - [X] run locally and tested manually - [x] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s) ## Potential risks There are lots of potentially subtle behaviors from the `strings.Split` (and `strings.Fields`) usage that should be accounted for. If our existing tests don't cover those subtleties, there's risk for introducing an unintended bug. More intricate handling/parsing previously using the `strings` package should get extra attention from reviewers. I've attempted to break up my changes into logical commit chunks to aid in review / help spot potentially concerning changes.
This commit is contained in:
@@ -81,17 +81,14 @@ func CamelCaseToSnakeCase(s string) string {
|
||||
}
|
||||
|
||||
func SnakeCaseToPascalCase(s string) string {
|
||||
// Split the string by underscores
|
||||
words := strings.Split(s, "_")
|
||||
|
||||
// Capitalize the first letter of each word
|
||||
for i, word := range words {
|
||||
var b strings.Builder
|
||||
// Capitalize the first letter of each word split by underscore
|
||||
for word := range strings.SplitSeq(s, "_") {
|
||||
// Convert first rune to upper and the rest to lower case
|
||||
words[i] = cases.Title(language.AmericanEnglish).String(strings.ToLower(word))
|
||||
b.WriteString(cases.Title(language.AmericanEnglish).String(strings.ToLower(word)))
|
||||
}
|
||||
|
||||
// Join them back into a single string
|
||||
return strings.Join(words, "")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func IsASCIIUpper(c rune) bool {
|
||||
|
||||
33
cmd/tools/codegen/helpers_test.go
Normal file
33
cmd/tools/codegen/helpers_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package codegen
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSnakeCaseToPascalCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "empty", in: "", want: ""},
|
||||
{name: "single_lower", in: "a", want: "A"},
|
||||
{name: "single_upper", in: "A", want: "A"},
|
||||
{name: "simple", in: "hello", want: "Hello"},
|
||||
{name: "two_words", in: "hello_world", want: "HelloWorld"},
|
||||
{name: "all_caps", in: "HELLO_WORLD", want: "HelloWorld"},
|
||||
{name: "leading_underscore", in: "_leading", want: "Leading"},
|
||||
{name: "trailing_underscore", in: "trailing_", want: "Trailing"},
|
||||
{name: "double_underscore", in: "a__b", want: "AB"},
|
||||
{name: "only_underscores", in: "__", want: ""},
|
||||
{name: "common_id", in: "user_id", want: "UserId"},
|
||||
{name: "with_digits", in: "http_server_v2", want: "HttpServerV2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := SnakeCaseToPascalCase(tt.in)
|
||||
if got != tt.want {
|
||||
t.Fatalf("SnakeCaseToPascalCase(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func findProtoImports() []string {
|
||||
if d.Type().IsRegular() && strings.HasSuffix(path, ".proto") {
|
||||
protoFile, err := os.ReadFile(path)
|
||||
fatalIfErr(err)
|
||||
for _, line := range strings.Split(string(protoFile), "\n") {
|
||||
for line := range strings.SplitSeq(string(protoFile), "\n") {
|
||||
if match := matchImport.FindStringSubmatch(line); len(match) > 0 {
|
||||
i := match[1]
|
||||
if strings.HasPrefix(i, "temporal/api/") ||
|
||||
|
||||
@@ -152,7 +152,7 @@ func newRunIDPrecondition(runID string) connector.Precondition {
|
||||
}
|
||||
|
||||
if strings.Contains(fileName, runID) {
|
||||
fileNameParts := strings.Split(fileName, "_")
|
||||
fileNameParts := strings.SplitN(fileName, "_", 5)
|
||||
if len(fileNameParts) != 5 {
|
||||
return true
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func newWorkflowIDPrecondition(workflowID string) connector.Precondition {
|
||||
}
|
||||
|
||||
if strings.Contains(fileName, workflowID) {
|
||||
fileNameParts := strings.Split(fileName, "_")
|
||||
fileNameParts := strings.SplitN(fileName, "_", 5)
|
||||
if len(fileNameParts) != 5 {
|
||||
return true
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func newWorkflowTypeNamePrecondition(workflowTypeName string) connector.Precondi
|
||||
}
|
||||
|
||||
if strings.Contains(fileName, workflowTypeName) {
|
||||
fileNameParts := strings.Split(fileName, "_")
|
||||
fileNameParts := strings.SplitN(fileName, "_", 5)
|
||||
if len(fileNameParts) != 5 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package s3store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -223,11 +224,17 @@ func (v *visibilityArchiver) queryAll(
|
||||
nextPageToken: nextPageToken,
|
||||
parsedQuery: &parsedQuery{},
|
||||
}, saTypeMap, searchPrefix, func(key string) bool {
|
||||
// We only want to return entries for the closeTimeout secondary index, which will always be of the form:
|
||||
// .../closeTimeout/<closeTimeout>/<runID>, so we split the key on "/" and check that the third-to-last
|
||||
// element is "closeTimeout".
|
||||
elements := strings.Split(key, "/")
|
||||
return len(elements) >= 3 && elements[len(elements)-3] == secondaryIndexKeyCloseTimeout
|
||||
// We only want to return entries for the closeTimeout secondary index. Keys for this
|
||||
// index are always of the form:
|
||||
// .../closeTimeout/<timestamp>/<runID>
|
||||
// Walk from the end instead of splitting the whole string to avoid unnecessary
|
||||
// allocations and to keep the logic clear:
|
||||
// - drop <runID>
|
||||
// - drop <timestamp>
|
||||
// - check the remaining last segment equals "closeTimeout".
|
||||
dir := path.Dir(key) // drop runID
|
||||
dir = path.Dir(dir) // drop <timestamp>
|
||||
return path.Base(dir) == secondaryIndexKeyCloseTimeout
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -125,7 +125,7 @@ func (a *defaultJWTClaimMapper) extractPermissions(permissions []interface{}, cl
|
||||
}
|
||||
parts = []string{match[a.matchNamespaceIndex], match[a.matchRoleIndex]}
|
||||
} else {
|
||||
parts = strings.Split(p, ":")
|
||||
parts = strings.SplitN(p, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
a.logger.Warn(fmt.Sprintf("ignoring permission in unexpected format: %v", permission))
|
||||
continue
|
||||
|
||||
@@ -2,7 +2,6 @@ package headers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
@@ -152,8 +151,15 @@ func (vc *versionChecker) ClientSupported(ctx context.Context) error {
|
||||
// given feature (which should be one of the Feature... constants above).
|
||||
func (vc *versionChecker) ClientSupportsFeature(ctx context.Context, feature string) bool {
|
||||
headers := GetValues(ctx, SupportedFeaturesHeaderName)
|
||||
clientFeatures := strings.Split(headers[0], SupportedFeaturesHeaderDelim)
|
||||
return slices.Contains(clientFeatures, feature)
|
||||
if len(headers) == 0 {
|
||||
return false
|
||||
}
|
||||
for clientFeature := range strings.SplitSeq(headers[0], SupportedFeaturesHeaderDelim) {
|
||||
if clientFeature == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mustParseRanges(ranges map[string]string) map[string]semver.Range {
|
||||
|
||||
@@ -1385,15 +1385,16 @@ func BuildHistoryGarbageCleanupInfo(namespaceID, workflowID, runID string) strin
|
||||
|
||||
// SplitHistoryGarbageCleanupInfo returns workflow identity information
|
||||
func SplitHistoryGarbageCleanupInfo(info string) (namespaceID, workflowID, runID string, err error) {
|
||||
ss := strings.Split(info, ":")
|
||||
// workflowID can contain ":" so len(ss) can be greater than 3
|
||||
if len(ss) < numItemsInGarbageInfo {
|
||||
// Expect format: namespaceID:workflowID:runID, but workflowID may contain ':' so we
|
||||
// take everything between the first and last ':' as workflowID.
|
||||
first := strings.IndexByte(info, ':')
|
||||
last := strings.LastIndexByte(info, ':')
|
||||
if first < 0 || first == last { // need at least two ':' to have 3 parts
|
||||
return "", "", "", fmt.Errorf("not able to split info for %s", info)
|
||||
}
|
||||
namespaceID = ss[0]
|
||||
runID = ss[len(ss)-1]
|
||||
workflowEnd := len(info) - len(runID) - 1
|
||||
workflowID = info[len(namespaceID)+1 : workflowEnd]
|
||||
namespaceID = info[:first]
|
||||
workflowID = info[first+1 : last]
|
||||
runID = info[last+1:]
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ func GetHistoryTaskQueueName(
|
||||
}
|
||||
|
||||
func GetHistoryTaskQueueCategoryID(queueName string) (int, error) {
|
||||
fields := strings.Split(queueName, "_")
|
||||
fields := strings.SplitN(queueName, "_", 4)
|
||||
if len(fields) != 4 {
|
||||
return 0, fmt.Errorf("%w: %s", ErrInvalidQueueName, queueName)
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func ConfigureCassandraCluster(cfg config.Cassandra, cluster *gocql.ClusterConfi
|
||||
// parseHosts returns parses a list of hosts separated by comma
|
||||
func parseHosts(input string) []string {
|
||||
var hosts []string
|
||||
for _, h := range strings.Split(input, ",") {
|
||||
for h := range strings.SplitSeq(input, ",") {
|
||||
if host := strings.TrimSpace(h); len(host) > 0 {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func (mdb *db) processRowFromDB(row *sqlplugin.VisibilityRow) error {
|
||||
for saName, saValue := range *row.SearchAttributes {
|
||||
switch typedSaValue := saValue.(type) {
|
||||
case string:
|
||||
if strings.Index(typedSaValue, keywordListSeparator) >= 0 {
|
||||
if strings.Contains(typedSaValue, keywordListSeparator) {
|
||||
// If the string contains the keywordListSeparator, then we need to split it
|
||||
// into a list of keywords.
|
||||
(*row.SearchAttributes)[saName] = strings.Split(typedSaValue, keywordListSeparator)
|
||||
|
||||
@@ -122,9 +122,8 @@ func formatComparisonExprStringForError(expr sqlparser.ComparisonExpr) string {
|
||||
// Simple tokenizer by spaces. It's a temporary solution as it doesn't cover tokenizer used by
|
||||
// PostgreSQL or SQLite.
|
||||
func tokenizeTextQueryString(s string) []string {
|
||||
tokens := strings.Split(s, " ")
|
||||
nonEmptyTokens := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
nonEmptyTokens := make([]string, 0, strings.Count(s, " ")+1)
|
||||
for token := range strings.SplitSeq(s, " ") {
|
||||
if token != "" {
|
||||
nonEmptyTokens = append(nonEmptyTokens, token)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_tokenizeTextQueryString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "single token",
|
||||
input: "foo",
|
||||
want: []string{"foo"},
|
||||
},
|
||||
{
|
||||
name: "two tokens",
|
||||
input: "foo bar",
|
||||
want: []string{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
name: "multiple spaces collapsed",
|
||||
input: "a b c",
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "leading and trailing spaces",
|
||||
input: " foo bar ",
|
||||
want: []string{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
name: "spaces only",
|
||||
input: " ",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "tabs are not separators",
|
||||
input: "foo\tbar baz",
|
||||
want: []string{"foo\tbar", "baz"},
|
||||
},
|
||||
{
|
||||
name: "newlines are not separators",
|
||||
input: "foo\nbar baz",
|
||||
want: []string{"foo\nbar", "baz"},
|
||||
},
|
||||
{
|
||||
name: "simple comparison with quotes",
|
||||
input: "status = \"RUNNING\"",
|
||||
want: []string{"status", "=", "\"RUNNING\""},
|
||||
},
|
||||
{
|
||||
name: "comparison without spaces around operator",
|
||||
input: "status='RUNNING' AND execution_time > 2025-01-01",
|
||||
want: []string{"status='RUNNING'", "AND", "execution_time", ">", "2025-01-01"},
|
||||
},
|
||||
{
|
||||
name: "quoted string with internal space",
|
||||
input: "WorkflowId = 'abc 123' AND RunId = 'xyz'",
|
||||
want: []string{"WorkflowId", "=", "'abc", "123'", "AND", "RunId", "=", "'xyz'"},
|
||||
},
|
||||
{
|
||||
name: "IS NULL and datetime literal",
|
||||
input: "CloseTime IS NULL OR CloseTime > '2024-01-01T00:00:00Z'",
|
||||
want: []string{"CloseTime", "IS", "NULL", "OR", "CloseTime", ">", "'2024-01-01T00:00:00Z'"},
|
||||
},
|
||||
{
|
||||
name: "LIKE pattern",
|
||||
input: "name LIKE '%abc%'",
|
||||
want: []string{"name", "LIKE", "'%abc%'"},
|
||||
},
|
||||
{
|
||||
name: "parentheses and operators",
|
||||
input: "(status = 'FAILED') AND (attempts >= 3)",
|
||||
want: []string{"(status", "=", "'FAILED')", "AND", "(attempts", ">=", "3)"},
|
||||
},
|
||||
{
|
||||
name: "IN with list including spaced item",
|
||||
input: "NamespaceId in ('ns1','ns 2','ns3')",
|
||||
want: []string{"NamespaceId", "in", "('ns1','ns", "2','ns3')"},
|
||||
},
|
||||
{
|
||||
name: "json containment operator",
|
||||
input: "attr_json @> '{\"key\":\"value\"}'",
|
||||
want: []string{"attr_json", "@>", "'{\"key\":\"value\"}'"},
|
||||
},
|
||||
{
|
||||
name: "combined IN and comparison",
|
||||
input: "execution_status IN ('RUNNING','COMPLETED') AND startTime >= 2025-01-01T00:00:00Z",
|
||||
want: []string{"execution_status", "IN", "('RUNNING','COMPLETED')", "AND", "startTime", ">=", "2025-01-01T00:00:00Z"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := tokenizeTextQueryString(test.input)
|
||||
require.Equal(t, test.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func SpanExportersFromEnv(
|
||||
return exporters, nil
|
||||
}
|
||||
|
||||
for _, exporterType := range strings.Split(exporterTypes, ",") {
|
||||
for exporterType := range strings.SplitSeq(exporterTypes, ",") {
|
||||
switch SpanExporterType(exporterType) {
|
||||
case OtelTracesOtlpExporterType:
|
||||
// only grpc is supported; fail if user requests a different protocol
|
||||
|
||||
@@ -9,21 +9,27 @@ import (
|
||||
)
|
||||
|
||||
func ConvertPathToCamel(input string) []string {
|
||||
pathParts := strings.Split(input, ".")
|
||||
for i, path := range pathParts {
|
||||
var pathParts []string
|
||||
for path := range strings.SplitSeq(input, ".") {
|
||||
// Split by "_" and convert each word to Title Case (CamelCase)
|
||||
words := strings.Split(path, "_")
|
||||
snakeCase := len(words) > 1
|
||||
for j, word := range words {
|
||||
if snakeCase && j > 0 {
|
||||
words[j] = cases.Title(language.Und).String(strings.ToLower(word))
|
||||
var b strings.Builder
|
||||
j := 0
|
||||
for word := range strings.SplitSeq(path, "_") {
|
||||
if j > 0 {
|
||||
b.WriteString(cases.Title(language.Und).String(strings.ToLower(word)))
|
||||
} else {
|
||||
// lowercase the first letter
|
||||
words[j] = strings.ToLower(word[:1]) + word[1:]
|
||||
if len(word) > 0 {
|
||||
b.WriteString(strings.ToLower(word[:1]))
|
||||
if len(word) > 1 {
|
||||
b.WriteString(word[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
j++
|
||||
}
|
||||
// Join the words into a CamelCase substring
|
||||
pathParts[i] = strings.Join(words, "")
|
||||
pathParts = append(pathParts, b.String())
|
||||
}
|
||||
// Join all CamelCase substrings back with "."
|
||||
return pathParts
|
||||
|
||||
@@ -22,12 +22,14 @@ func WildCardStringsToRegexp(patterns []string) (*regexp.Regexp, error) {
|
||||
result.WriteRune('^')
|
||||
for i, pattern := range patterns {
|
||||
result.WriteRune('(')
|
||||
for i, literal := range strings.Split(pattern, "*") {
|
||||
if i > 0 {
|
||||
first := true
|
||||
for literal := range strings.SplitSeq(pattern, "*") {
|
||||
if !first {
|
||||
// Replace * with .*
|
||||
result.WriteString(".*")
|
||||
}
|
||||
result.WriteString(regexp.QuoteMeta(literal))
|
||||
first = false
|
||||
}
|
||||
result.WriteRune(')')
|
||||
if i < len(patterns)-1 {
|
||||
|
||||
@@ -95,12 +95,14 @@ func allowedAddressConverter(val any) ([]AddressMatchRule, error) {
|
||||
func addressPatternToRegexp(pattern string) string {
|
||||
var result strings.Builder
|
||||
result.WriteString("^")
|
||||
for i, literal := range strings.Split(pattern, "*") {
|
||||
if i > 0 {
|
||||
first := true
|
||||
for literal := range strings.SplitSeq(pattern, "*") {
|
||||
if !first {
|
||||
// Replace * with .*
|
||||
result.WriteString(".*")
|
||||
}
|
||||
result.WriteString(regexp.QuoteMeta(literal))
|
||||
first = false
|
||||
}
|
||||
result.WriteString("$")
|
||||
return result.String()
|
||||
|
||||
39
components/callbacks/config_test.go
Normal file
39
components/callbacks/config_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_addressPatternToRegexp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
want string
|
||||
}{
|
||||
{name: "empty", pattern: "", want: "^$"},
|
||||
{name: "no_wildcard", pattern: "foo", want: "^foo$"},
|
||||
{name: "single_wildcard_only", pattern: "*", want: "^.*$"},
|
||||
{name: "leading_wildcard", pattern: "*foo", want: "^.*foo$"},
|
||||
{name: "trailing_wildcard", pattern: "foo*", want: "^foo.*$"},
|
||||
{name: "surrounded_wildcard", pattern: "*foo*", want: "^.*foo.*$"},
|
||||
{name: "middle_wildcard", pattern: "foo*bar", want: "^foo.*bar$"},
|
||||
{name: "literal_dots_around_wildcard", pattern: "foo.*bar", want: "^foo\\..*bar$"},
|
||||
{name: "prefix_subdomain", pattern: "prefix.*.domain", want: "^prefix\\..*\\.domain$"},
|
||||
{name: "leading_any_subdomain", pattern: "*.example.com", want: "^.*\\.example\\.com$"},
|
||||
{name: "host_with_port", pattern: "api.example.com:8080", want: "^api\\.example\\.com:8080$"},
|
||||
{name: "consecutive_wildcards", pattern: "a**b", want: "^a.*.*b$"},
|
||||
{name: "triple_wildcards", pattern: "a***b", want: "^a.*.*.*b$"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := addressPatternToRegexp(test.pattern)
|
||||
require.Equal(t, test.want, got)
|
||||
_, err := regexp.Compile(got)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -402,8 +402,10 @@ func (c *requestContext) augmentContext(ctx context.Context, header http.Header)
|
||||
func() string { return methodNameForMetrics },
|
||||
)
|
||||
if userAgent := header.Get(http.CanonicalHeaderKey(headerUserAgent)); userAgent != "" {
|
||||
parts := strings.Split(userAgent, clientNameVersionDelim)
|
||||
if len(parts) == 2 {
|
||||
// Preserve original strict behavior: only process if exactly one delimiter present.
|
||||
if strings.Count(userAgent, clientNameVersionDelim) == 1 {
|
||||
parts := strings.SplitN(userAgent, clientNameVersionDelim, 2)
|
||||
if len(parts) == 2 { // defensive
|
||||
mdIncoming, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
mdIncoming = metadata.MD{}
|
||||
@@ -413,6 +415,7 @@ func (c *requestContext) augmentContext(ctx context.Context, header http.Header)
|
||||
ctx = metadata.NewIncomingContext(ctx, mdIncoming)
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers.Propagate(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +126,12 @@ func (c *operationContext) augmentContext(ctx context.Context, header nexus.Head
|
||||
func() string { return c.method },
|
||||
)
|
||||
if userAgent, ok := header[headerUserAgent]; ok {
|
||||
parts := strings.Split(userAgent, clientNameVersionDelim)
|
||||
if len(parts) == 2 {
|
||||
// Use SplitN for efficiency but enforce exactly one delimiter to preserve the
|
||||
// original (pre-SplitN) strictness where additional delimiters cause us to ignore
|
||||
// the header instead of coalescing trailing data into the version string.
|
||||
if strings.Count(userAgent, clientNameVersionDelim) == 1 { // exact single occurrence
|
||||
parts := strings.SplitN(userAgent, clientNameVersionDelim, 2)
|
||||
if len(parts) == 2 { // always true given Count==1, kept for defensive clarity
|
||||
mdIncoming, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
mdIncoming = metadata.MD{}
|
||||
@@ -137,6 +141,7 @@ func (c *operationContext) augmentContext(ctx context.Context, header nexus.Head
|
||||
ctx = metadata.NewIncomingContext(ctx, mdIncoming)
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers.Propagate(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -270,14 +270,26 @@ func parseCronString(c string) (*schedulepb.StructuredCalendarSpec, *schedulepb.
|
||||
|
||||
// split fields
|
||||
cal := schedulepb.CalendarSpec{Comment: comment}
|
||||
fields := strings.Fields(c)
|
||||
switch len(fields) {
|
||||
// Use FieldsSeq to avoid building an unbounded slice; we only accept 5–7 fields.
|
||||
const maxCronFields = 7
|
||||
var toks [maxCronFields]string
|
||||
n := 0
|
||||
for tok := range strings.FieldsSeq(c) {
|
||||
if n < maxCronFields {
|
||||
toks[n] = tok
|
||||
n++
|
||||
continue
|
||||
}
|
||||
// More than 7 fields → invalid.
|
||||
return nil, nil, "", errors.New("CronString does not have 5-7 fields")
|
||||
}
|
||||
switch n {
|
||||
case 5:
|
||||
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek = fields[0], fields[1], fields[2], fields[3], fields[4]
|
||||
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek = toks[0], toks[1], toks[2], toks[3], toks[4]
|
||||
case 6:
|
||||
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]
|
||||
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = toks[0], toks[1], toks[2], toks[3], toks[4], toks[5]
|
||||
case 7:
|
||||
cal.Second, cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6]
|
||||
cal.Second, cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = toks[0], toks[1], toks[2], toks[3], toks[4], toks[5], toks[6]
|
||||
default:
|
||||
return nil, nil, "", errors.New("CronString does not have 5-7 fields")
|
||||
}
|
||||
@@ -404,15 +416,23 @@ func makeRange(s, field, def string, minVal, maxVal int, parseMode parseMode) ([
|
||||
return nil, nil // special case for year: all is represented as empty range list
|
||||
}
|
||||
var ranges []*schedulepb.Range
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
for part := range strings.SplitSeq(s, ",") {
|
||||
var err error
|
||||
step := 1
|
||||
hasStep := false
|
||||
if strings.Contains(part, "/") {
|
||||
skipParts := strings.Split(part, "/")
|
||||
if len(skipParts) != 2 {
|
||||
slashes := strings.Count(part, "/")
|
||||
if slashes > 1 {
|
||||
// Inputs like "3/5/7" should yield the canonical "too many slashes" error
|
||||
// (instead of a later strconv parse error) so tests get consistent results.
|
||||
return nil, fmt.Errorf("%s has too many slashes", field)
|
||||
}
|
||||
if slashes == 1 {
|
||||
// A single slash introduces an integer step.
|
||||
skipParts := strings.SplitN(part, "/", 2)
|
||||
// Count==1 guarantees len==2; only need to ensure the right side is non-empty.
|
||||
if skipParts[1] == "" { // e.g. "5/"
|
||||
return nil, fmt.Errorf("%s missing step value", field)
|
||||
}
|
||||
part = skipParts[0]
|
||||
step, err = strconv.Atoi(skipParts[1])
|
||||
if err != nil {
|
||||
@@ -427,7 +447,13 @@ func makeRange(s, field, def string, minVal, maxVal int, parseMode parseMode) ([
|
||||
start, end := minVal, maxVal
|
||||
if part != "*" {
|
||||
if strings.Contains(part, "-") {
|
||||
rangeParts := strings.Split(part, "-")
|
||||
// Only a single dash is allowed to denote a range (e.g. "1-5").
|
||||
// Inputs with multiple dashes like "1-5-7" should raise the
|
||||
// canonical "too many dashes" error expected by tests.
|
||||
if strings.Count(part, "-") > 1 { // no negative numbers are expected in spec
|
||||
return nil, fmt.Errorf("%s has too many dashes", field)
|
||||
}
|
||||
rangeParts := strings.SplitN(part, "-", 2)
|
||||
if len(rangeParts) != 2 {
|
||||
return nil, fmt.Errorf("%s has too many dashes", field)
|
||||
}
|
||||
|
||||
@@ -185,11 +185,9 @@ func parseOptionsMap(value string) map[string]string {
|
||||
return make(map[string]string)
|
||||
}
|
||||
|
||||
split := strings.Split(value, ",")
|
||||
|
||||
parsedMap := make(map[string]string)
|
||||
|
||||
for _, pair := range split {
|
||||
for pair := range strings.SplitSeq(value, ",") {
|
||||
trimmedPair := strings.ReplaceAll(pair, " ", "")
|
||||
if len(trimmedPair) == 0 {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user