From 2a709aaf6103efd5a76d075a8f4d5968cadbbbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Moyne?= Date: Thu, 18 Aug 2022 09:52:28 -0700 Subject: [PATCH] - Changes to make adding new mapping functions easier (#3305) * - Changes to make adding new mapping functions easier - Adds new subject mapping functions: {{SplitFromLeft(wildcard index, position)}} {{SplitFromRight(wildcard index, position)}} {{SliceFromLeft(wildcard index, slice size)}} {{SliceFromRight(wildcard index, slice size)}} {{Split(wildcard index, deliminator)}} Examples: shouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45") shouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345") shouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0") shouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890") shouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi") shouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o") - Subject mapping functions can now be all lower case or Pascal case (or a combination): e.g. splitfromleft, SplitFromLeft, splitFromleft, etc... --- server/accounts.go | 285 +++++++++++++++++++++++++++++++--------- server/accounts_test.go | 55 +++++--- server/sublist.go | 8 +- server/sublist_test.go | 2 + 4 files changed, 270 insertions(+), 80 deletions(-) diff --git a/server/accounts.go b/server/accounts.go index a7081f25..eb4e2ca2 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -166,8 +166,26 @@ const ( ) var commaSeparatorRegEx = regexp.MustCompile(`,\s*`) -var partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*partition\s*\((.*)\)\s*}}`) -var wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*wildcard\s*\((.*)\)\s*}}`) +var partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*[pP]artition\s*\((.*)\)\s*}}`) +var wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*[wW]ildcard\s*\((.*)\)\s*}}`) +var splitFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[lL]eft\s*\((.*)\)\s*}}`) +var splitFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[rR]ight\s*\((.*)\)\s*}}`) +var sliceFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[lL]eft\s*\((.*)\)\s*}}`) +var sliceFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[rR]ight\s*\((.*)\)\s*}}`) +var splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`) + +// Enum for the subject mapping transform function types +const ( + NoTransform int16 = iota + BadTransform + Partition + Wildcard + SplitFromLeft + SplitFromRight + SliceFromLeft + SliceFromRight + Split +) // String helper. func (rt ServiceRespType) String() string { @@ -845,7 +863,7 @@ func (a *Account) selectMappedSubject(dest string) (string, bool) { } if d != nil { - if len(d.tr.dtpi) == 0 { + if len(d.tr.dtokmftokindexesargs) == 0 { ndest = d.tr.dest } else if nsubj, err := d.tr.transform(tts); err == nil { ndest = nsubj @@ -948,7 +966,7 @@ func (a *Account) removeClient(c *client) int { func setExportAuth(ea *exportAuth, subject string, accounts []*Account, accountPos uint) error { if accountPos > 0 { - token := strings.Split(subject, ".") + token := strings.Split(subject, tsep) if len(token) < int(accountPos) || token[accountPos-1] != "*" { return ErrInvalidSubject } @@ -2442,7 +2460,7 @@ func (a *Account) AddStreamExport(subject string, accounts []*Account) error { // AddStreamExport will add an export to the account. If accounts is nil // it will signify a public export, meaning anyone can import. // if accountPos is > 0, all imports will be granted where the following holds: -// strings.Split(subject, ".")[accountPos] == account id will be granted. +// strings.Split(subject, tsep)[accountPos] == account id will be granted. func (a *Account) addStreamExportWithAccountPos(subject string, accounts []*Account, accountPos uint) error { if a == nil { return ErrMissingAccount @@ -4166,11 +4184,13 @@ func (dr *CacheDirAccResolver) Reload() error { // These can also be used for proper mapping on wildcard exports/imports. // These will be grouped and caching and locking are assumed to be in the upper layers. type transform struct { - src, dest string - dtoks []string - stoks []string - dtpi [][]int // destination token position indexes - dtpinp []int32 // destination token position index number of partitions + src, dest string + dtoks []string // destination tokens + stoks []string // source tokens + dtokmftypes []int16 // destination token mapping function types + dtokmftokindexesargs [][]int // destination token mapping function array of source token index arguments + dtokmfintargs []int32 // destination token mapping function int32 arguments + dtokmfstringargs []string // destination token mapping function string arguments } func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string { @@ -4181,17 +4201,40 @@ func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string return nil } -// Helper to pull raw place holder indexes and number of partitions. Returns -1 if not a place holder. -func placeHolderIndex(token string) ([]int, int32, error) { +// Helper for mapping functions that take a wildcard index and an integer as arguments +func transformIndexIntArgsHelper(token string, args []string, transformType int16) (int16, []int, int32, string, error) { + if len(args) < 2 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " ")) + if err != nil { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + + return transformType, []int{i}, int32(mappingFunctionIntArg), _EMPTY_, nil +} + +// Helper to ingest and index the transform destination token (e.g. $x or {{}}) in the token +// returns a transformation type, and three function arguments: an array of source subject token indexes, and a single number (e.g. number of partitions, or a slice size), and a string (e.g.a split delimiter) + +func indexPlaceHolders(token string) (int16, []int, int32, string, error) { length := len(token) if length > 1 { // old $1, $2, etc... mapping format still supported to maintain backwards compatibility if token[0] == '$' { // simple non-partition mapping tp, err := strconv.Atoi(token[1:]) if err != nil { - return []int{-1}, -1, nil + // other things rely on tokens starting with $ so not an error just leave it as is + return NoTransform, []int{-1}, -1, _EMPTY_, nil } - return []int{tp}, -1, nil + return Wildcard, []int{tp}, -1, _EMPTY_, nil } // New 'mustache' style mapping @@ -4200,45 +4243,92 @@ func placeHolderIndex(token string) ([]int, int32, error) { args := getMappingFunctionArgs(wildcardMappingFunctionRegEx, token) if args != nil { if len(args) == 1 && args[0] == _EMPTY_ { - return []int{-1}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} } if len(args) == 1 { - tp, err := strconv.Atoi(strings.Trim(args[0], " ")) + tokenIndex, err := strconv.Atoi(strings.Trim(args[0], " ")) if err != nil { - return []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } - return []int{tp}, -1, nil + return Wildcard, []int{tokenIndex}, -1, _EMPTY_, nil } else { - return []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} } } + // partition(number of partitions, token1, token2, ...) args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token) if args != nil { if len(args) < 2 { - return []int{-1}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} } if len(args) >= 2 { - tphnp, err := strconv.Atoi(strings.Trim(args[0], " ")) + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) if err != nil { - return []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } var numPositions = len(args[1:]) - tps := make([]int, numPositions) + tokenIndexes := make([]int, numPositions) for ti, t := range args[1:] { i, err := strconv.Atoi(strings.Trim(t, " ")) if err != nil { - return []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } - tps[ti] = i + tokenIndexes[ti] = i } - return tps, int32(tphnp), nil + + return Partition, tokenIndexes, int32(mappingFunctionIntArg), _EMPTY_, nil } } - return []int{}, -1, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction} + + // SplitFromLeft(token, position) + args = getMappingFunctionArgs(splitFromLeftMappingFunctionRegEx, token) + if args != nil { + return transformIndexIntArgsHelper(token, args, SplitFromLeft) + } + + // SplitFromRight(token, position) + args = getMappingFunctionArgs(splitFromRightMappingFunctionRegEx, token) + if args != nil { + return transformIndexIntArgsHelper(token, args, SplitFromRight) + } + + // SliceFromLeft(token, position) + args = getMappingFunctionArgs(sliceFromLeftMappingFunctionRegEx, token) + if args != nil { + return transformIndexIntArgsHelper(token, args, SliceFromLeft) + } + + // SliceFromRight(token, position) + args = getMappingFunctionArgs(sliceFromRightMappingFunctionRegEx, token) + if args != nil { + return transformIndexIntArgsHelper(token, args, SliceFromRight) + } + + // split(token, deliminator) + args = getMappingFunctionArgs(splitMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + if strings.Contains(args[1], " ") || strings.Contains(args[1], tsep) { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token: token, err: ErrorMappingDestinationFunctionInvalidArgument} + } + + return Split, []int{i}, -1, args[1], nil + } + + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction} } } - return []int{-1}, -1, nil + return NoTransform, []int{-1}, -1, _EMPTY_, nil } // SubjectTransformer transforms subjects using mappings @@ -4266,8 +4356,10 @@ func newTransform(src, dest string) (*transform, error) { return nil, ErrBadSubject } - var dtpi [][]int - var dtpinb []int32 + var dtokMappingFunctionTypes []int16 + var dtokMappingFunctionTokenIndexes [][]int + var dtokMappingFunctionIntArgs []int32 + var dtokMappingFunctionStringArgs []string // If the src has partial wildcards then the dest needs to have the token place markers. if npwcs > 0 || hasFwc { @@ -4281,25 +4373,31 @@ func newTransform(src, dest string) (*transform, error) { nphs := 0 for _, token := range dtokens { - tp, nb, err := placeHolderIndex(token) + tranformType, transformArgWildcardIndexes, transfomArgInt, transformArgString, err := indexPlaceHolders(token) if err != nil { return nil, err } - if tp[0] >= 0 { + + if tranformType == NoTransform { + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform) + dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1}) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) + } else { nphs++ // Now build up our runtime mapping from dest to source tokens. var stis []int - for _, position := range tp { - if position > npwcs { - return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, position), ErrorMappingDestinationFunctionWildcardIndexOutOfRange} + for _, wildcardIndex := range transformArgWildcardIndexes { + if wildcardIndex > npwcs { + return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, wildcardIndex), ErrorMappingDestinationFunctionWildcardIndexOutOfRange} } - stis = append(stis, sti[position]) + stis = append(stis, sti[wildcardIndex]) } - dtpi = append(dtpi, stis) - dtpinb = append(dtpinb, nb) - } else { - dtpi = append(dtpi, []int{-1}) - dtpinb = append(dtpinb, -1) + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType) + dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, stis) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, transformArgString) + } } if nphs < npwcs { @@ -4308,7 +4406,7 @@ func newTransform(src, dest string) (*transform, error) { } } - return &transform{src: src, dest: dest, dtoks: dtokens, stoks: stokens, dtpi: dtpi, dtpinp: dtpinb}, nil + return &transform{src: src, dest: dest, dtoks: dtokens, stoks: stokens, dtokmftypes: dtokMappingFunctionTypes, dtokmftokindexesargs: dtokMappingFunctionTokenIndexes, dtokmfintargs: dtokMappingFunctionIntArgs, dtokmfstringargs: dtokMappingFunctionStringArgs}, nil } // Match will take a literal published subject that is associated with a client and will match and transform @@ -4370,41 +4468,110 @@ func (tr *transform) getHashPartition(key []byte, numBuckets int) string { // Do a transform on the subject to the dest subject. func (tr *transform) transform(tokens []string) (string, error) { - if len(tr.dtpi) == 0 { + if len(tr.dtokmftypes) == 0 { return tr.dest, nil } var b strings.Builder - var token string - // We need to walk destination tokens and create the mapped subject pulling tokens from src. + // We need to walk destination tokens and create the mapped subject pulling tokens or mapping functions // This is slow and that is ok, transforms should have caching layer in front for mapping transforms // and export/import semantics with streams and services. - li := len(tr.dtpi) - 1 - for i, index := range tr.dtpi { - // <0 means use destination token. - if index[0] < 0 { - token = tr.dtoks[i] + li := len(tr.dtokmftypes) - 1 + for i, mfType := range tr.dtokmftypes { + if mfType == NoTransform { // Break if fwc - if len(token) == 1 && token[0] == fwc { + if len(tr.dtoks[i]) == 1 && tr.dtoks[i][0] == fwc { break } + b.WriteString(tr.dtoks[i]) } else { - // >= 0 means use source map index to figure out which source token to pull. - if tr.dtpinp[i] > 0 { // there is a valid (i.e. not -1) value for number of partitions, this is a partition transform token + switch mfType { + case Partition: var ( _buffer [64]byte keyForHashing = _buffer[:0] ) - for _, sourceToken := range tr.dtpi[i] { + for _, sourceToken := range tr.dtokmftokindexesargs[i] { keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...) } - token = tr.getHashPartition(keyForHashing, int(tr.dtpinp[i])) - } else { // back to normal substitution - token = tokens[tr.dtpi[i][0]] + b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i]))) + case Wildcard: // simple substitution + b.WriteString(tokens[tr.dtokmftokindexesargs[i][0]]) + case SplitFromLeft: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + position := int(tr.dtokmfintargs[i]) + if position > 0 && position < sourceTokenLen { + b.WriteString(sourceToken[:position]) + b.WriteString(tsep) + b.WriteString(sourceToken[position:]) + } else { // too small to split at the requested position: don't split + b.WriteString(sourceToken) + } + case SplitFromRight: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + position := int(tr.dtokmfintargs[i]) + if position > 0 && position < sourceTokenLen { + b.WriteString(sourceToken[:sourceTokenLen-position]) + b.WriteString(tsep) + b.WriteString(sourceToken[sourceTokenLen-position:]) + } else { // too small to split at the requested position: don't split + b.WriteString(sourceToken) + } + case SliceFromLeft: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + sliceSize := int(tr.dtokmfintargs[i]) + if sliceSize > 0 && sliceSize < sourceTokenLen { + for i := 0; i+sliceSize <= sourceTokenLen; i += sliceSize { + if i != 0 { + b.WriteString(tsep) + } + b.WriteString(sourceToken[i : i+sliceSize]) + if i+sliceSize != sourceTokenLen && i+sliceSize+sliceSize > sourceTokenLen { + b.WriteString(tsep) + b.WriteString(sourceToken[i+sliceSize:]) + break + } + } + } else { // too small to slice at the requested size: don't slice + b.WriteString(sourceToken) + } + case SliceFromRight: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + sliceSize := int(tr.dtokmfintargs[i]) + if sliceSize > 0 && sliceSize < sourceTokenLen { + remainder := sourceTokenLen % sliceSize + if remainder > 0 { + b.WriteString(sourceToken[:remainder]) + b.WriteString(tsep) + } + for i := remainder; i+sliceSize <= sourceTokenLen; i += sliceSize { + b.WriteString(sourceToken[i : i+sliceSize]) + if i+sliceSize < sourceTokenLen { + b.WriteString(tsep) + } + } + } else { // too small to slice at the requested size: don't slice + b.WriteString(sourceToken) + } + case Split: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + splits := strings.Split(sourceToken, tr.dtokmfstringargs[i]) + for j, split := range splits { + if split != _EMPTY_ { + b.WriteString(split) + } + if j < len(splits)-1 && splits[j+1] != _EMPTY_ && j != 0 { + b.WriteString(tsep) + } + } } } - b.WriteString(token) + if i < li { b.WriteByte(btsep) } @@ -4424,7 +4591,7 @@ func (tr *transform) transform(tokens []string) (string, error) { // Reverse a transform. func (tr *transform) reverse() *transform { - if len(tr.dtpi) == 0 { + if len(tr.dtokmftokindexesargs) == 0 { rtr, _ := newTransform(tr.dest, tr.src) return rtr } diff --git a/server/accounts_test.go b/server/accounts_test.go index 435b418b..f9bc8623 100644 --- a/server/accounts_test.go +++ b/server/accounts_test.go @@ -50,47 +50,54 @@ func simpleAccountServer(t *testing.T) (*Server, *Account, *Account) { func TestPlaceHolderIndex(t *testing.T) { testString := "$1" - indexes, nbPartitions, err := placeHolderIndex(testString) + transformType, indexes, nbPartitions, _, err := indexPlaceHolders(testString) + var position int32 - if err != nil || len(indexes) != 1 || indexes[0] != 1 || nbPartitions != -1 { + if err != nil || transformType != Wildcard || len(indexes) != 1 || indexes[0] != 1 || nbPartitions != -1 { t.Fatalf("Error parsing %s", testString) } testString = "{{partition(10,1,2,3)}}" - indexes, nbPartitions, err = placeHolderIndex(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) - if err != nil || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { + if err != nil || transformType != Partition || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { t.Fatalf("Error parsing %s", testString) } - testString = "{{ partition(10,1,2,3) }}" + testString = "{{ Partition (10,1,2,3) }}" - indexes, nbPartitions, err = placeHolderIndex(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) - if err != nil || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { - t.Fatalf("Error parsing %s", testString) - } - - testString = "{{partition (10,1,2,3)}}" - - indexes, nbPartitions, err = placeHolderIndex(testString) - - if err != nil || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { + if err != nil || transformType != Partition || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { t.Fatalf("Error parsing %s", testString) } testString = "{{wildcard(2)}}" - indexes, nbPartitions, err = placeHolderIndex(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) - if err != nil || len(indexes) != 1 || indexes[0] != 2 || nbPartitions != -1 { + if err != nil || transformType != Wildcard || len(indexes) != 1 || indexes[0] != 2 || nbPartitions != -1 { t.Fatalf("Error parsing %s", testString) } - testString = "{{ wildcard (2) }}" - indexes, nbPartitions, err = placeHolderIndex(testString) + testString = "{{SplitFromLeft(2,1)}}" + transformType, indexes, position, _, err = indexPlaceHolders(testString) - if err != nil || len(indexes) != 1 || indexes[0] != 2 || nbPartitions != -1 { + if err != nil || transformType != SplitFromLeft || len(indexes) != 1 || indexes[0] != 2 || position != 1 { + t.Fatalf("Error parsing %s", testString) + } + + testString = "{{SplitFromRight(3,2)}}" + transformType, indexes, position, _, err = indexPlaceHolders(testString) + + if err != nil || transformType != SplitFromRight || len(indexes) != 1 || indexes[0] != 3 || position != 2 { + t.Fatalf("Error parsing %s", testString) + } + + testString = "{{SliceFromLeft(2,2)}}" + transformType, indexes, sliceSize, _, err := indexPlaceHolders(testString) + + if err != nil || transformType != SliceFromLeft || len(indexes) != 1 || indexes[0] != 2 || sliceSize != 2 { t.Fatalf("Error parsing %s", testString) } } @@ -3290,6 +3297,7 @@ func TestSubjectTransforms(t *testing.T) { shouldErr("foo.*", "foo.{{wildcard()}}") // Not enough arguments passed to the mapping function shouldErr("foo.*", "foo.{{wildcard(1,2)}}") // Too many arguments passed to the mapping function shouldErr("foo.*", "foo.{{ wildcard5) }}") // Bad mapping function + shouldErr("foo.*", "foo.{{splitLeft(2,2}}") // arg out of range shouldBeOK := func(src, dest string) *transform { t.Helper() @@ -3303,6 +3311,7 @@ func TestSubjectTransforms(t *testing.T) { shouldBeOK("foo", "bar") shouldBeOK("foo.*.bar.*.baz", "req.$2.$1") shouldBeOK("baz.>", "mybaz.>") + shouldBeOK("*", "{{splitfromleft(1,1)}}") shouldMatch := func(src, dest, sample, expected string) { t.Helper() @@ -3321,6 +3330,12 @@ func TestSubjectTransforms(t *testing.T) { shouldMatch("baz.>", "my.pre.>", "baz.1.2.3", "my.pre.1.2.3") shouldMatch("baz.>", "foo.bar.>", "baz.1.2.3", "foo.bar.1.2.3") shouldMatch("*", "foo.bar.$1", "foo", "foo.bar.foo") + shouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45") + shouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345") + shouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0") + shouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890") + shouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi") + shouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o") // combo + checks split for multiple instance of deliminator and deliminator being at the start or end } func TestAccountSystemPermsWithGlobalAccess(t *testing.T) { diff --git a/server/sublist.go b/server/sublist.go index 99230dde..bae47306 100644 --- a/server/sublist.go +++ b/server/sublist.go @@ -1140,7 +1140,13 @@ func ValidateMappingDestination(subject string) error { } if length > 4 && t[0] == '{' && t[1] == '{' && t[length-2] == '}' && t[length-1] == '}' { - if !partitionMappingFunctionRegEx.MatchString(t) && !wildcardMappingFunctionRegEx.MatchString(t) { + if !partitionMappingFunctionRegEx.MatchString(t) && + !wildcardMappingFunctionRegEx.MatchString(t) && + !splitFromLeftMappingFunctionRegEx.MatchString(t) && + !splitFromRightMappingFunctionRegEx.MatchString(t) && + !sliceFromLeftMappingFunctionRegEx.MatchString(t) && + !sliceFromRightMappingFunctionRegEx.MatchString(t) && + !splitMappingFunctionRegEx.MatchString(t) { return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction} } else { continue diff --git a/server/sublist_test.go b/server/sublist_test.go index 05a14dd2..f1fb1fde 100644 --- a/server/sublist_test.go +++ b/server/sublist_test.go @@ -669,6 +669,8 @@ func TestValidateDestinationSubject(t *testing.T) { checkError(ValidateMappingDestination("foo.{{ wildcard(1) }}"), nil, t) checkError(ValidateMappingDestination("foo.{{wildcard( 1 )}}"), nil, t) checkError(ValidateMappingDestination("foo.{{partition(2,1)}}"), nil, t) + checkError(ValidateMappingDestination("foo.{{SplitFromLeft(2,1)}}"), nil, t) + checkError(ValidateMappingDestination("foo.{{SplitFromRight(2,1)}}"), nil, t) checkError(ValidateMappingDestination("foo.{{unknown(1)}}"), ErrInvalidMappingDestination, t) checkError(ValidateMappingDestination("foo..}"), ErrInvalidMappingDestination, t) checkError(ValidateMappingDestination("foo. bar}"), ErrInvalidMappingDestinationSubject, t)