1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-21 20:13:22 +00:00
fiber/path.go

337 lines
12 KiB
Go
Raw Normal View History

// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 Github Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
2020-06-15 13:36:50 +02:00
// ⚠️ This path parser was inspired by ucarion/urlpath (MIT License).
// 💖 Maintained and modified for Fiber by @renewerner87
package fiber
import (
2020-08-09 19:50:10 +02:00
"strconv"
"strings"
"github.com/gofiber/fiber/v2/utils"
)
2020-08-09 19:50:10 +02:00
// routeParser holds the path segments and param names
type routeParser struct {
2020-09-13 11:20:11 +02:00
segs []*routeSegment // the parsed segments of the route
params []string // that parameter names the parsed route
wildCardCount int // number of wildcard parameters, used internally to give the wildcard parameter its number
plusCount int // number of plus parameters, used internally to give the plus parameter its number
}
// paramsSeg holds the segment metadata
2020-08-06 02:36:05 +02:00
type routeSegment struct {
2020-08-10 10:14:17 +02:00
// const information
Const string // constant part of the route
// parameter information
IsParam bool // Truth value that indicates whether it is a parameter or a constant part
ParamName string // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added
2020-08-09 19:50:10 +02:00
ComparePart string // search part to find the end of the parameter
PartCount int // how often is the search part contained in the non-param segments? -> necessary for greedy search
2020-08-10 10:14:17 +02:00
IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus
IsOptional bool // indicates whether the parameter is optional or not
// common information
2020-09-13 11:20:11 +02:00
IsLast bool // shows if the segment is the last one for the route
HasOptionalSlash bool // segment has the possibility of an optional slash
Length int // length of the parameter for segment, when its 0 then the length is undetermined
2020-08-10 10:14:17 +02:00
// future TODO: add support for optional groups "/abc(/def)?"
}
2020-08-10 10:14:17 +02:00
// different special routing signs
2020-08-06 02:36:05 +02:00
const (
2020-08-10 10:14:17 +02:00
wildcardParam byte = '*' // indicates a optional greedy parameter
plusParam byte = '+' // indicates a required greedy parameter
optionalParam byte = '?' // concludes a parameter by name and makes it optional
paramStarterChar byte = ':' // start character for a parameter with name
slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional
2020-08-06 02:36:05 +02:00
)
2020-08-10 10:14:17 +02:00
// list of possible parameter and segment delimiter
2020-08-06 02:36:05 +02:00
var (
// slash has a special role, unlike the other parameters it must not be interpreted as a parameter
routeDelimiter = []byte{slashDelimiter, '-', '.'}
// list of chars for the parameter recognising
2020-08-07 21:42:53 +02:00
parameterStartChars = []byte{wildcardParam, plusParam, paramStarterChar}
2020-08-10 10:14:17 +02:00
// list of chars of delimiters and the starting parameter name char
2020-08-06 02:36:05 +02:00
parameterDelimiterChars = append([]byte{paramStarterChar}, routeDelimiter...)
// list of chars to find the end of a parameter
parameterEndChars = append([]byte{optionalParam}, parameterDelimiterChars...)
)
// parseRoute analyzes the route and divides it into segments for constant areas and parameters,
// this information is needed later when assigning the requests to the declared routes
2020-08-06 02:36:05 +02:00
func parseRoute(pattern string) routeParser {
2020-08-09 19:50:10 +02:00
parser := routeParser{}
2020-08-06 02:36:05 +02:00
part := ""
for len(pattern) > 0 {
nextParamPosition := findNextParamPosition(pattern)
// handle the parameter part
if nextParamPosition == 0 {
2020-08-09 19:50:10 +02:00
processedPart, seg := parser.analyseParameterPart(pattern)
parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart
} else {
2020-08-09 19:50:10 +02:00
processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition)
parser.segs, part = append(parser.segs, seg), processedPart
}
2020-08-06 02:36:05 +02:00
// reduce the pattern by the processed parts
if len(part) == len(pattern) {
break
}
2020-08-06 02:36:05 +02:00
pattern = pattern[len(part):]
}
// mark last segment
2020-08-09 19:50:10 +02:00
if len(parser.segs) > 0 {
parser.segs[len(parser.segs)-1].IsLast = true
2020-08-06 02:36:05 +02:00
}
2020-08-09 19:50:10 +02:00
parser.segs = addParameterMetaInfo(parser.segs)
2020-08-09 19:50:10 +02:00
return parser
}
// addParameterMetaInfo add important meta information to the parameter segments
// to simplify the search for the end of the parameter
2020-09-13 11:20:11 +02:00
func addParameterMetaInfo(segs []*routeSegment) []*routeSegment {
2020-08-09 19:50:10 +02:00
comparePart := ""
2020-09-13 11:20:11 +02:00
segLen := len(segs)
2020-08-09 19:50:10 +02:00
// loop from end to begin
2020-09-13 11:20:11 +02:00
for i := segLen - 1; i >= 0; i-- {
2020-08-09 19:50:10 +02:00
// set the compare part for the parameter
if segs[i].IsParam {
2020-08-10 10:14:17 +02:00
// important for finding the end of the parameter
2020-08-09 19:50:10 +02:00
segs[i].ComparePart = comparePart
} else {
comparePart = segs[i].Const
if len(comparePart) > 1 {
comparePart = utils.TrimRight(comparePart, slashDelimiter)
}
}
}
// loop from begin to end
2020-09-13 11:20:11 +02:00
for i := 0; i < segLen; i++ {
2020-08-09 19:50:10 +02:00
// check how often the compare part is in the following const parts
2020-09-13 11:20:11 +02:00
if segs[i].IsParam {
// check if parameter segments are directly after each other and if one of them is greedy
// in case the next parameter or the current parameter is not a wildcard its not greedy, we only want one character
if segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy {
segs[i].Length = 1
}
if segs[i].ComparePart == "" {
continue
}
2020-08-10 10:14:17 +02:00
for j := i + 1; j <= len(segs)-1; j++ {
2020-08-09 19:50:10 +02:00
if !segs[j].IsParam {
2020-08-10 10:14:17 +02:00
// count is important for the greedy match
2020-08-09 19:50:10 +02:00
segs[i].PartCount += strings.Count(segs[j].Const, segs[i].ComparePart)
}
}
2020-09-13 11:20:11 +02:00
// check if the end of the segment is a optional slash and then if the segement is optional or the last one
} 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
2020-08-09 19:50:10 +02:00
}
}
return segs
2020-08-06 02:36:05 +02:00
}
// findNextParamPosition search for the next possible parameter start position
func findNextParamPosition(pattern string) int {
nextParamPosition := findNextCharsetPosition(pattern, parameterStartChars)
if nextParamPosition != -1 && len(pattern) > nextParamPosition && pattern[nextParamPosition] != wildcardParam {
// search for parameter characters for the found parameter start,
// if there are more, move the parameter start to the last parameter char
for found := findNextCharsetPosition(pattern[nextParamPosition+1:], parameterStartChars); found == 0; {
nextParamPosition++
if len(pattern) > nextParamPosition {
break
}
}
}
2020-08-06 02:36:05 +02:00
return nextParamPosition
}
// analyseConstantPart find the end of the constant part and create the route segment
2020-09-13 11:20:11 +02:00
func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) {
2020-08-06 02:36:05 +02:00
// handle the constant part
processedPart := pattern
if nextParamPosition != -1 {
// remove the constant part until the parameter
processedPart = pattern[:nextParamPosition]
}
2020-09-13 11:20:11 +02:00
return processedPart, &routeSegment{
Const: processedPart,
Length: len(processedPart),
}
2020-08-06 02:36:05 +02:00
}
2020-08-06 02:36:05 +02:00
// analyseParameterPart find the parameter end and create the route segment
2020-09-13 11:20:11 +02:00
func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) {
2020-08-06 02:36:05 +02:00
isWildCard := pattern[0] == wildcardParam
2020-08-07 21:42:53 +02:00
isPlusParam := pattern[0] == plusParam
2020-08-06 02:36:05 +02:00
parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars)
2020-08-10 10:14:17 +02:00
2020-08-06 02:36:05 +02:00
// handle wildcard end
2020-08-07 21:42:53 +02:00
if isWildCard || isPlusParam {
2020-08-06 02:36:05 +02:00
parameterEndPosition = 0
} else if parameterEndPosition == -1 {
parameterEndPosition = len(pattern) - 1
2020-08-09 22:05:14 +02:00
} else if !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars) {
2020-08-06 02:36:05 +02:00
parameterEndPosition = parameterEndPosition + 1
}
// cut params part
processedPart := pattern[0 : parameterEndPosition+1]
paramName := GetTrimmedParam(processedPart)
2020-08-09 19:50:10 +02:00
// add access iterator to wildcard and plus
if isWildCard {
routeParser.wildCardCount++
paramName += strconv.Itoa(routeParser.wildCardCount)
} else if isPlusParam {
routeParser.plusCount++
paramName += strconv.Itoa(routeParser.plusCount)
}
2020-09-13 11:20:11 +02:00
return processedPart, &routeSegment{
2020-08-09 19:50:10 +02:00
ParamName: paramName,
2020-08-06 02:36:05 +02:00
IsParam: true,
IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam,
2020-08-07 21:42:53 +02:00
IsGreedy: isWildCard || isPlusParam,
2020-08-06 02:36:05 +02:00
}
}
2020-08-06 02:36:05 +02:00
// isInCharset check is the given character in the charset list
func isInCharset(searchChar byte, charset []byte) bool {
for _, char := range charset {
if char == searchChar {
return true
}
}
return false
}
// findNextCharsetPosition search the next char position from the charset
func findNextCharsetPosition(search string, charset []byte) int {
nextPosition := -1
2020-08-06 02:36:05 +02:00
for _, char := range charset {
if pos := strings.IndexByte(search, char); pos != -1 && (pos < nextPosition || nextPosition == -1) {
nextPosition = pos
}
}
return nextPosition
}
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
2020-09-13 11:20:11 +02:00
func (routeParser *routeParser) getMatch(s, original string, params *[maxParams]string, partialCheck bool) bool {
var i, paramsIterator, partLen int
for _, segment := range routeParser.segs {
partLen = len(s)
2020-08-10 09:50:53 +02:00
// check const segment
2020-08-09 19:50:10 +02:00
if !segment.IsParam {
2020-09-13 11:20:11 +02:00
i = segment.Length
2020-08-07 21:42:53 +02:00
// is optional part or the const part must match with the given string
2020-09-13 11:20:11 +02:00
// check if the end of the segment is a optional slash
if segment.HasOptionalSlash && partLen == i-1 && s == segment.Const[:i-1] {
i--
} else if !(i <= partLen && s[:i] == segment.Const) {
return false
}
2020-08-09 19:50:10 +02:00
} else {
// determine parameter length
2020-09-13 11:20:11 +02:00
i = findParamLen(s, segment)
2020-08-09 19:50:10 +02:00
if !segment.IsOptional && i == 0 {
2020-09-13 11:20:11 +02:00
return false
2020-08-09 19:50:10 +02:00
}
// take over the params positions
2020-09-13 11:20:11 +02:00
params[paramsIterator] = original[:i]
2020-08-09 19:50:10 +02:00
paramsIterator++
}
// reduce founded part from the string
if partLen > 0 {
2020-09-13 11:20:11 +02:00
s, original = s[i:], original[i:]
}
}
if len(s) != 0 && !partialCheck {
2020-09-13 11:20:11 +02:00
return false
}
2020-09-13 11:20:11 +02:00
return true
}
2020-08-06 02:36:05 +02:00
// findParamLen for the expressjs wildcard behavior (right to left greedy)
// look at the other segments and take what is left for the wildcard from right to left
2020-09-13 11:20:11 +02:00
func findParamLen(s string, segment *routeSegment) int {
if segment.IsLast {
return findParamLenForLastSegment(s, segment)
2020-08-06 02:36:05 +02:00
}
2020-08-07 21:42:53 +02:00
2020-09-13 11:20:11 +02:00
if segment.Length != 0 && len(s) >= segment.Length {
return segment.Length
} else if segment.IsGreedy {
// Search the parameters until the next constant part
// special logic for greedy params
searchCount := strings.Count(s, segment.ComparePart)
2020-08-09 19:50:10 +02:00
if searchCount > 1 {
2020-09-13 11:20:11 +02:00
return findGreedyParamLen(s, searchCount, segment)
2020-08-07 14:38:40 +02:00
}
2020-08-09 19:50:10 +02:00
}
2020-08-07 21:42:53 +02:00
2020-09-13 11:20:11 +02:00
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 {
2020-08-09 19:50:10 +02:00
return constPosition
}
return len(s)
}
2020-08-07 21:42:53 +02:00
// findParamLenForLastSegment get the length of the parameter if it is the last segment
2020-09-13 11:20:11 +02:00
func findParamLenForLastSegment(s string, seg *routeSegment) int {
if !seg.IsGreedy {
if i := strings.IndexByte(s, slashDelimiter); i != -1 {
return i
}
2020-08-07 21:42:53 +02:00
}
return len(s)
}
// findGreedyParamLen get the length of the parameter for greedy segments from right to left
2020-09-13 11:20:11 +02:00
func findGreedyParamLen(s string, searchCount int, segment *routeSegment) int {
2020-08-07 21:42:53 +02:00
// check all from right to left segments
2020-08-09 19:50:10 +02:00
for i := segment.PartCount; i > 0 && searchCount > 0; i-- {
searchCount--
if constPosition := strings.LastIndex(s, segment.ComparePart); constPosition != -1 {
s = s[:constPosition]
} else {
break
2020-08-07 21:42:53 +02:00
}
}
return len(s)
}
// GetTrimmedParam trims the ':' & '?' from a string
func GetTrimmedParam(param string) string {
start := 0
end := len(param)
if param[start] != paramStarterChar { // is not a param
return param
}
start++
if param[end-1] == optionalParam { // is ?
end--
}
return param[start:end]
}