package fiber
import (
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"github.com/gofiber/fiber/v2/utils"
)
type routeParser struct {
segs []*routeSegment
params []string
wildCardCount int
plusCount int
}
type routeSegment struct {
Const string
IsParam bool
ParamName string
ComparePart string
PartCount int
IsGreedy bool
IsOptional bool
IsLast bool
HasOptionalSlash bool
Constraints []*Constraint
Length int
}
const (
wildcardParam byte = '*'
plusParam byte = '+'
optionalParam byte = '?'
paramStarterChar byte = ':'
slashDelimiter byte = '/'
escapeChar byte = '\\'
paramConstraintStart byte = '<'
paramConstraintEnd byte = '>'
paramConstraintSeparator byte = ';'
paramConstraintDataStart byte = '('
paramConstraintDataEnd byte = ')'
paramConstraintDataSeparator byte = ','
)
type TypeConstraint int16
type Constraint struct {
ID TypeConstraint
RegexCompiler *regexp .Regexp
Data []string
}
const (
noConstraint TypeConstraint = iota + 1
intConstraint
boolConstraint
floatConstraint
alphaConstraint
datetimeConstraint
guidConstraint
minLenConstraint
maxLenConstraint
lenConstraint
betweenLenConstraint
minConstraint
maxConstraint
rangeConstraint
regexConstraint
)
var (
routeDelimiter = []byte {slashDelimiter , '-' , '.' }
greedyParameters = []byte {wildcardParam , plusParam }
parameterStartChars = []byte {wildcardParam , plusParam , paramStarterChar }
parameterDelimiterChars = append ([]byte {paramStarterChar , escapeChar }, routeDelimiter ...)
parameterEndChars = append ([]byte {optionalParam }, parameterDelimiterChars ...)
parameterConstraintStartChars = []byte {paramConstraintStart }
parameterConstraintEndChars = []byte {paramConstraintEnd }
parameterConstraintSeparatorChars = []byte {paramConstraintSeparator }
parameterConstraintDataStartChars = []byte {paramConstraintDataStart }
parameterConstraintDataEndChars = []byte {paramConstraintDataEnd }
parameterConstraintDataSeparatorChars = []byte {paramConstraintDataSeparator }
)
func RoutePatternMatch (path , pattern string , cfg ...Config ) bool {
var ctxParams [maxParams ]string
config := Config {}
if len (cfg ) > 0 {
config = cfg [0 ]
}
if path == "" {
path = "/"
}
if pattern == "" {
pattern = "/"
}
if pattern [0 ] != '/' {
pattern = "/" + pattern
}
patternPretty := pattern
if !config .CaseSensitive {
patternPretty = utils .ToLower (patternPretty )
path = utils .ToLower (path )
}
if !config .StrictRouting && len (patternPretty ) > 1 {
patternPretty = utils .TrimRight (patternPretty , '/' )
}
parser := parseRoute (patternPretty )
if patternPretty == "/" && path == "/" {
return true
} else if patternPretty == "/*" {
return true
}
if len (parser .params ) > 0 {
if match := parser .getMatch (path , path , &ctxParams , false ); match {
return true
}
}
patternPretty = RemoveEscapeChar (patternPretty )
if len (patternPretty ) == len (path ) && patternPretty == path {
return true
}
return false
}
func parseRoute(pattern string ) routeParser {
parser := routeParser {}
part := ""
for len (pattern ) > 0 {
nextParamPosition := findNextParamPosition (pattern )
if nextParamPosition == 0 {
processedPart , seg := parser .analyseParameterPart (pattern )
parser .params , parser .segs , part = append (parser .params , seg .ParamName ), append (parser .segs , seg ), processedPart
} else {
processedPart , seg := parser .analyseConstantPart (pattern , nextParamPosition )
parser .segs , part = append (parser .segs , seg ), processedPart
}
if len (part ) == len (pattern ) {
break
}
pattern = pattern [len (part ):]
}
if len (parser .segs ) > 0 {
parser .segs [len (parser .segs )-1 ].IsLast = true
}
parser .segs = addParameterMetaInfo (parser .segs )
return parser
}
func addParameterMetaInfo(segs []*routeSegment ) []*routeSegment {
var comparePart string
segLen := len (segs )
for i := segLen - 1 ; i >= 0 ; i -- {
if segs [i ].IsParam {
segs [i ].ComparePart = RemoveEscapeChar (comparePart )
} else {
comparePart = segs [i ].Const
if len (comparePart ) > 1 {
comparePart = utils .TrimRight (comparePart , slashDelimiter )
}
}
}
for i := 0 ; i < segLen ; i ++ {
if segs [i ].IsParam {
if segLen > i +1 && !segs [i ].IsGreedy && segs [i +1 ].IsParam && !segs [i +1 ].IsGreedy {
segs [i ].Length = 1
}
if segs [i ].ComparePart == "" {
continue
}
for j := i + 1 ; j <= len (segs )-1 ; j ++ {
if !segs [j ].IsParam {
segs [i ].PartCount += strings .Count (segs [j ].Const , segs [i ].ComparePart )
}
}
} else if segs [i ].Const [len (segs [i ].Const )-1 ] == slashDelimiter && (segs [i ].IsLast || (segLen > i +1 && segs [i +1 ].IsOptional )) {
segs [i ].HasOptionalSlash = true
}
}
return segs
}
func findNextParamPosition(pattern string ) int {
nextParamPosition := findNextNonEscapedCharsetPosition (pattern , parameterStartChars )
if nextParamPosition != -1 && len (pattern ) > nextParamPosition && pattern [nextParamPosition ] != wildcardParam {
for found := findNextNonEscapedCharsetPosition (pattern [nextParamPosition +1 :], parameterStartChars ); found == 0 ; {
nextParamPosition ++
if len (pattern ) > nextParamPosition {
break
}
}
}
return nextParamPosition
}
func (*routeParser ) analyseConstantPart (pattern string , nextParamPosition int ) (string , *routeSegment ) {
processedPart := pattern
if nextParamPosition != -1 {
processedPart = pattern [:nextParamPosition ]
}
constPart := RemoveEscapeChar (processedPart )
return processedPart , &routeSegment {
Const : constPart ,
Length : len (constPart ),
}
}
func (routeParser *routeParser ) analyseParameterPart (pattern string ) (string , *routeSegment ) {
isWildCard := pattern [0 ] == wildcardParam
isPlusParam := pattern [0 ] == plusParam
var parameterEndPosition int
if strings .ContainsRune (pattern , rune (paramConstraintStart )) && strings .ContainsRune (pattern , rune (paramConstraintEnd )) {
parameterEndPosition = findNextCharsetPositionConstraint (pattern [1 :], parameterEndChars )
} else {
parameterEndPosition = findNextNonEscapedCharsetPosition (pattern [1 :], parameterEndChars )
}
parameterConstraintStart := -1
parameterConstraintEnd := -1
switch {
case isWildCard , isPlusParam :
parameterEndPosition = 0
case parameterEndPosition == -1 :
parameterEndPosition = len (pattern ) - 1
case !isInCharset (pattern [parameterEndPosition +1 ], parameterDelimiterChars ):
parameterEndPosition ++
}
if parameterEndPosition > 0 {
parameterConstraintStart = findNextNonEscapedCharsetPosition (pattern [0 :parameterEndPosition ], parameterConstraintStartChars )
parameterConstraintEnd = findLastCharsetPosition (pattern [0 :parameterEndPosition +1 ], parameterConstraintEndChars )
}
processedPart := pattern [0 : parameterEndPosition +1 ]
paramName := RemoveEscapeChar (GetTrimmedParam (processedPart ))
var constraints []*Constraint
if hasConstraint := parameterConstraintStart != -1 && parameterConstraintEnd != -1 ; hasConstraint {
constraintString := pattern [parameterConstraintStart +1 : parameterConstraintEnd ]
userConstraints := splitNonEscaped (constraintString , string (parameterConstraintSeparatorChars ))
constraints = make ([]*Constraint , 0 , len (userConstraints ))
for _ , c := range userConstraints {
start := findNextNonEscapedCharsetPosition (c , parameterConstraintDataStartChars )
end := findLastCharsetPosition (c , parameterConstraintDataEndChars )
if start != -1 && end != -1 {
constraint := &Constraint {
ID : getParamConstraintType (c [:start ]),
}
if constraint .ID != regexConstraint {
constraint .Data = splitNonEscaped (c [start +1 :end ], string (parameterConstraintDataSeparatorChars ))
if len (constraint .Data ) == 1 {
constraint .Data [0 ] = RemoveEscapeChar (constraint .Data [0 ])
} else if len (constraint .Data ) == 2 {
constraint .Data [0 ] = RemoveEscapeChar (constraint .Data [0 ])
constraint .Data [1 ] = RemoveEscapeChar (constraint .Data [1 ])
}
}
if constraint .ID == regexConstraint {
constraint .Data = []string {c [start +1 : end ]}
constraint .RegexCompiler = regexp .MustCompile (constraint .Data [0 ])
}
constraints = append (constraints , constraint )
} else {
constraints = append (constraints , &Constraint {
ID : getParamConstraintType (c ),
Data : []string {},
})
}
}
paramName = RemoveEscapeChar (GetTrimmedParam (pattern [0 :parameterConstraintStart ]))
}
if isWildCard {
routeParser .wildCardCount ++
paramName += strconv .Itoa (routeParser .wildCardCount )
} else if isPlusParam {
routeParser .plusCount ++
paramName += strconv .Itoa (routeParser .plusCount )
}
segment := &routeSegment {
ParamName : paramName ,
IsParam : true ,
IsOptional : isWildCard || pattern [parameterEndPosition ] == optionalParam ,
IsGreedy : isWildCard || isPlusParam ,
}
if len (constraints ) > 0 {
segment .Constraints = constraints
}
return processedPart , segment
}
func isInCharset(searchChar byte , charset []byte ) bool {
for _ , char := range charset {
if char == searchChar {
return true
}
}
return false
}
func findNextCharsetPosition(search string , charset []byte ) int {
nextPosition := -1
for _ , char := range charset {
if pos := strings .IndexByte (search , char ); pos != -1 && (pos < nextPosition || nextPosition == -1 ) {
nextPosition = pos
}
}
return nextPosition
}
func findLastCharsetPosition(search string , charset []byte ) int {
lastPosition := -1
for _ , char := range charset {
if pos := strings .LastIndexByte (search , char ); pos != -1 && (pos < lastPosition || lastPosition == -1 ) {
lastPosition = pos
}
}
return lastPosition
}
func findNextCharsetPositionConstraint(search string , charset []byte ) int {
constraintStart := findNextNonEscapedCharsetPosition (search , parameterConstraintStartChars )
constraintEnd := findNextNonEscapedCharsetPosition (search , parameterConstraintEndChars )
nextPosition := -1
for _ , char := range charset {
pos := strings .IndexByte (search , char )
if pos != -1 && (pos < nextPosition || nextPosition == -1 ) {
if (pos > constraintStart && pos > constraintEnd ) || (pos < constraintStart && pos < constraintEnd ) {
nextPosition = pos
}
}
}
return nextPosition
}
func findNextNonEscapedCharsetPosition(search string , charset []byte ) int {
pos := findNextCharsetPosition (search , charset )
for pos > 0 && search [pos -1 ] == escapeChar {
if len (search ) == pos +1 {
return -1
}
nextPossiblePos := findNextCharsetPosition (search [pos +1 :], charset )
if nextPossiblePos == -1 {
return -1
}
pos = nextPossiblePos + pos + 1
}
return pos
}
func splitNonEscaped(s , sep string ) []string {
var result []string
i := findNextNonEscapedCharsetPosition (s , []byte (sep ))
for i > -1 {
result = append (result , s [:i ])
s = s [i +len (sep ):]
i = findNextNonEscapedCharsetPosition (s , []byte (sep ))
}
return append (result , s )
}
func (routeParser *routeParser ) getMatch (detectionPath , path string , params *[maxParams ]string , partialCheck bool ) bool {
var i , paramsIterator , partLen int
for _ , segment := range routeParser .segs {
partLen = len (detectionPath )
if !segment .IsParam {
i = segment .Length
if segment .HasOptionalSlash && partLen == i -1 && detectionPath == segment .Const [:i -1 ] {
i --
} else if !(i <= partLen && detectionPath [:i ] == segment .Const ) {
return false
}
} else {
i = findParamLen (detectionPath , segment )
if !segment .IsOptional && i == 0 {
return false
}
params [paramsIterator ] = path [:i ]
if !(segment .IsOptional && i == 0 ) {
for _ , c := range segment .Constraints {
if matched := c .CheckConstraint (params [paramsIterator ]); !matched {
return false
}
}
}
paramsIterator ++
}
if partLen > 0 {
detectionPath , path = detectionPath [i :], path [i :]
}
}
if detectionPath != "" && !partialCheck {
return false
}
return true
}
func findParamLen(s string , segment *routeSegment ) int {
if segment .IsLast {
return findParamLenForLastSegment (s , segment )
}
if segment .Length != 0 && len (s ) >= segment .Length {
return segment .Length
} else if segment .IsGreedy {
searchCount := strings .Count (s , segment .ComparePart )
if searchCount > 1 {
return findGreedyParamLen (s , searchCount , segment )
}
}
if len (segment .ComparePart ) == 1 {
if constPosition := strings .IndexByte (s , segment .ComparePart [0 ]); constPosition != -1 {
return constPosition
}
} else if constPosition := strings .Index (s , segment .ComparePart ); constPosition != -1 {
if !segment .IsGreedy && strings .IndexByte (s [:constPosition ], slashDelimiter ) != -1 {
return 0
}
return constPosition
}
return len (s )
}
func findParamLenForLastSegment(s string , seg *routeSegment ) int {
if !seg .IsGreedy {
if i := strings .IndexByte (s , slashDelimiter ); i != -1 {
return i
}
}
return len (s )
}
func findGreedyParamLen(s string , searchCount int , segment *routeSegment ) int {
for i := segment .PartCount ; i > 0 && searchCount > 0 ; i -- {
searchCount --
if constPosition := strings .LastIndex (s , segment .ComparePart ); constPosition != -1 {
s = s [:constPosition ]
} else {
break
}
}
return len (s )
}
func GetTrimmedParam (param string ) string {
start := 0
end := len (param )
if end == 0 || param [start ] != paramStarterChar {
return param
}
start ++
if param [end -1 ] == optionalParam {
end --
}
return param [start :end ]
}
func RemoveEscapeChar (word string ) string {
if strings .IndexByte (word , escapeChar ) != -1 {
return strings .ReplaceAll (word , string (escapeChar ), "" )
}
return word
}
func getParamConstraintType(constraintPart string ) TypeConstraint {
switch constraintPart {
case ConstraintInt :
return intConstraint
case ConstraintBool :
return boolConstraint
case ConstraintFloat :
return floatConstraint
case ConstraintAlpha :
return alphaConstraint
case ConstraintGuid :
return guidConstraint
case ConstraintMinLen , ConstraintMinLenLower :
return minLenConstraint
case ConstraintMaxLen , ConstraintMaxLenLower :
return maxLenConstraint
case ConstraintLen :
return lenConstraint
case ConstraintBetweenLen , ConstraintBetweenLenLower :
return betweenLenConstraint
case ConstraintMin :
return minConstraint
case ConstraintMax :
return maxConstraint
case ConstraintRange :
return rangeConstraint
case ConstraintDatetime :
return datetimeConstraint
case ConstraintRegex :
return regexConstraint
default :
return noConstraint
}
}
func (c *Constraint ) CheckConstraint (param string ) bool {
var err error
var num int
needOneData := []TypeConstraint {minLenConstraint , maxLenConstraint , lenConstraint , minConstraint , maxConstraint , datetimeConstraint , regexConstraint }
needTwoData := []TypeConstraint {betweenLenConstraint , rangeConstraint }
for _ , data := range needOneData {
if c .ID == data && len (c .Data ) == 0 {
return false
}
}
for _ , data := range needTwoData {
if c .ID == data && len (c .Data ) < 2 {
return false
}
}
switch c .ID {
case noConstraint :
case intConstraint :
_, err = strconv .Atoi (param )
case boolConstraint :
_, err = strconv .ParseBool (param )
case floatConstraint :
_, err = strconv .ParseFloat (param , 32 )
case alphaConstraint :
for _ , r := range param {
if !unicode .IsLetter (r ) {
return false
}
}
case guidConstraint :
_, err = uuid .Parse (param )
case minLenConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
if len (param ) < data {
return false
}
case maxLenConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
if len (param ) > data {
return false
}
case lenConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
if len (param ) != data {
return false
}
case betweenLenConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
data2 , _ := strconv .Atoi (c .Data [1 ])
length := len (param )
if length < data || length > data2 {
return false
}
case minConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
num , err = strconv .Atoi (param )
if num < data {
return false
}
case maxConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
num , err = strconv .Atoi (param )
if num > data {
return false
}
case rangeConstraint :
data , _ := strconv .Atoi (c .Data [0 ])
data2 , _ := strconv .Atoi (c .Data [1 ])
num , err = strconv .Atoi (param )
if num < data || num > data2 {
return false
}
case datetimeConstraint :
_, err = time .Parse (c .Data [0 ], param )
case regexConstraint :
if match := c .RegexCompiler .MatchString (param ); !match {
return false
}
}
return err == nil
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .