-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathpath.go
249 lines (221 loc) · 7.03 KB
/
path.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 Github Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
// ⚠️ This path parser was inspired by ucarion/urlpath (MIT License).
// 💖 Maintained and modified for Fiber by @renewerner87
package fiber
import (
"strings"
"sync/atomic"
utils "github.com/gofiber/utils"
)
// routeParser holds the path segments and param names
type routeParser struct {
segs []paramSeg
params []string
}
// paramsSeg holds the segment metadata
type paramSeg struct {
Param string
Const string
IsParam bool
IsOptional bool
IsLast bool
EndChar byte
}
// list of possible parameter and segment delimiter
// slash has a special role, unlike the other parameters it must not be interpreted as a parameter
var routeDelimiter = []byte{'/', '-', '.'}
const wildcardParam string = "*"
// 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
func parseRoute(pattern string) (p routeParser) {
var out []paramSeg
var params []string
part, delimiterPos := "", 0
for len(pattern) > 0 && delimiterPos != -1 {
delimiterPos = findNextRouteSegmentEnd(pattern)
if delimiterPos != -1 {
part = pattern[:delimiterPos]
} else {
part = pattern
}
partLen, lastSeg := len(part), len(out)-1
if partLen == 0 { // skip empty parts
if len(pattern) > 0 {
// remove first char
pattern = pattern[1:]
}
continue
}
// is parameter ?
if part[0] == '*' || part[0] == ':' {
out = append(out, paramSeg{
Param: utils.GetTrimmedParam(part),
IsParam: true,
IsOptional: part == wildcardParam || part[partLen-1] == '?',
})
lastSeg = len(out) - 1
params = append(params, out[lastSeg].Param)
// combine const segments
} else if lastSeg >= 0 && !out[lastSeg].IsParam {
out[lastSeg].Const += string(out[lastSeg].EndChar) + part
// create new const segment
} else {
out = append(out, paramSeg{
Const: part,
})
lastSeg = len(out) - 1
}
if delimiterPos != -1 && len(pattern) >= delimiterPos+1 {
out[lastSeg].EndChar = pattern[delimiterPos]
pattern = pattern[delimiterPos+1:]
} else {
// last default char
out[lastSeg].EndChar = '/'
}
}
if len(out) > 0 {
out[len(out)-1].IsLast = true
}
p = routeParser{segs: out, params: params}
return
}
// findNextRouteSegmentEnd searches in the route for the next end position for a segment
func findNextRouteSegmentEnd(search string) int {
nextPosition := -1
for _, delimiter := range routeDelimiter {
if pos := strings.IndexByte(search, delimiter); 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
func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) {
lenKeys := len(p.params)
paramsPositions := getAllocFreeParamsPos(lenKeys)
var i, j, paramsIterator, partLen, paramStart int
if len(s) > 0 {
s = s[1:]
paramStart++
}
for index, segment := range p.segs {
partLen = len(s)
// check parameter
if segment.IsParam {
// determine parameter length
if segment.Param == wildcardParam {
if segment.IsLast {
i = partLen
} else {
i = findWildcardParamLen(s, p.segs, index)
}
} else {
i = strings.IndexByte(s, segment.EndChar)
}
if i == -1 {
i = partLen
}
if !segment.IsOptional && i == 0 {
return nil, false
// special case for not slash end character
} else if i > 0 && partLen >= i && segment.EndChar != '/' && s[i-1] == '/' {
return nil, false
}
paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i
paramsIterator++
} else {
// check const segment
i = len(segment.Const)
if partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const || (partLen > i && s[i] != segment.EndChar) {
return nil, false
}
}
// reduce founded part from the string
if partLen > 0 {
j = i + 1
if segment.IsLast || partLen < j {
j = i
}
paramStart += j
s = s[j:]
}
}
if len(s) != 0 && !partialCheck {
return nil, false
}
return paramsPositions, true
}
// paramsForPos get parameters for the given positions from the given path
func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string {
size := len(paramsPositions)
params := getAllocFreeParams(size)
for i, positions := range paramsPositions {
if positions[0] != positions[1] && len(path) >= positions[1] {
params[i] = path[positions[0]:positions[1]]
} else {
params[i] = ""
}
}
return params
}
// findWildcardParamLen 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
func findWildcardParamLen(s string, segments []paramSeg, currIndex int) int {
// "/api/*/:param" - "/api/joker/batman/robin/1" -> "joker/batman/robin", "1"
// "/api/*/:param" - "/api/joker/batman" -> "joker", "batman"
// "/api/*/:param" - "/api/joker-batman-robin/1" -> "joker-batman-robin", "1"
endChar := segments[currIndex].EndChar
neededEndChars := 0
// count the needed chars for the other segments
for i := currIndex + 1; i < len(segments); i++ {
if segments[i].EndChar == endChar {
neededEndChars++
}
}
// remove the part the other segments still need
for {
pos := strings.LastIndexByte(s, endChar)
if pos != -1 {
s = s[:pos]
}
neededEndChars--
if neededEndChars <= 0 || pos == -1 {
break
}
}
return len(s)
}
// performance tricks
// creates predefined arrays that are used to match the request routes so that no allocations need to be made
var paramsDummy, paramsPosDummy = make([]string, 100000), make([][2]int, 100000)
// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests
// to assign a separate range to each request
var startParamList, startParamPosList uint32 = 0, 0
// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use
func getAllocFreeParamsPos(allocLen int) [][2]int {
size := uint32(allocLen)
start := atomic.AddUint32(&startParamPosList, size)
if (start + 10) >= uint32(len(paramsPosDummy)) {
atomic.StoreUint32(&startParamPosList, 0)
return getAllocFreeParamsPos(allocLen)
}
start -= size
allocLen += int(start)
paramsPositions := paramsPosDummy[start:allocLen:allocLen]
return paramsPositions
}
// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use
func getAllocFreeParams(allocLen int) []string {
size := uint32(allocLen)
start := atomic.AddUint32(&startParamList, size)
if (start + 10) >= uint32(len(paramsPosDummy)) {
atomic.StoreUint32(&startParamList, 0)
return getAllocFreeParams(allocLen)
}
start -= size
allocLen += int(start)
params := paramsDummy[start:allocLen:allocLen]
return params
}