path.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. // ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
  2. // 📄 GitHub Repository: https://github.com/gofiber/fiber
  3. // 📌 API Documentation: https://docs.gofiber.io
  4. // ⚠️ This path parser was inspired by https://github.com/ucarion/urlpath
  5. // 💖 Maintained and modified for Fiber by @renewerner87
  6. package fiber
  7. import (
  8. "bytes"
  9. "fmt"
  10. "regexp"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "unicode"
  16. "github.com/gofiber/utils/v2"
  17. utilsbytes "github.com/gofiber/utils/v2/bytes"
  18. utilsstrings "github.com/gofiber/utils/v2/strings"
  19. "github.com/google/uuid"
  20. )
  21. // routeParser holds the path segments and param names
  22. type routeParser struct {
  23. segs []*routeSegment // the parsed segments of the route
  24. params []string // that parameter names the parsed route
  25. wildCardCount int // number of wildcard parameters, used internally to give the wildcard parameter its number
  26. plusCount int // number of plus parameters, used internally to give the plus parameter its number
  27. }
  28. var routerParserPool = &sync.Pool{
  29. New: func() any {
  30. return &routeParser{}
  31. },
  32. }
  33. // routeSegment holds the segment metadata
  34. type routeSegment struct {
  35. // const information
  36. Const string // constant part of the route
  37. ParamName string // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added
  38. ComparePart string // search part to find the end of the parameter
  39. Constraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default
  40. PartCount int // how often is the search part contained in the non-param segments? -> necessary for greedy search
  41. Length int // length of the parameter for segment, when its 0 then the length is undetermined
  42. // future TODO: add support for optional groups "/abc(/def)?"
  43. // parameter information
  44. IsParam bool // Truth value that indicates whether it is a parameter or a constant part
  45. IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus
  46. IsOptional bool // indicates whether the parameter is optional or not
  47. // common information
  48. IsLast bool // shows if the segment is the last one for the route
  49. HasOptionalSlash bool // segment has the possibility of an optional slash
  50. }
  51. // different special routing signs
  52. const (
  53. wildcardParam byte = '*' // indicates an optional greedy parameter
  54. plusParam byte = '+' // indicates a required greedy parameter
  55. optionalParam byte = '?' // concludes a parameter by name and makes it optional
  56. paramStarterChar byte = ':' // start character for a parameter with name
  57. slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional
  58. escapeChar byte = '\\' // escape character
  59. paramConstraintStart byte = '<' // start of type constraint for a parameter
  60. paramConstraintEnd byte = '>' // end of type constraint for a parameter
  61. paramConstraintSeparator byte = ';' // separator of type constraints for a parameter
  62. paramConstraintDataStart byte = '(' // start of data of type constraint for a parameter
  63. paramConstraintDataEnd byte = ')' // end of data of type constraint for a parameter
  64. paramConstraintDataSeparator byte = ',' // separator of data of type constraint for a parameter
  65. )
  66. // TypeConstraint parameter constraint types
  67. type TypeConstraint uint16
  68. // Constraint describes the validation rules that apply to a dynamic route
  69. // segment when matching incoming requests.
  70. type Constraint struct {
  71. RegexCompiler *regexp.Regexp
  72. Name string
  73. Data []string
  74. customConstraints []CustomConstraint
  75. ID TypeConstraint
  76. }
  77. // CustomConstraint is an interface for custom constraints
  78. type CustomConstraint interface {
  79. // Name returns the name of the constraint.
  80. // This name is used in the constraint matching.
  81. Name() string
  82. // Execute executes the constraint.
  83. // It returns true if the constraint is matched and right.
  84. // param is the parameter value to check.
  85. // args are the constraint arguments.
  86. Execute(param string, args ...string) bool
  87. }
  88. const (
  89. noConstraint TypeConstraint = 1 << iota
  90. intConstraint
  91. boolConstraint
  92. floatConstraint
  93. alphaConstraint
  94. datetimeConstraint
  95. guidConstraint
  96. minLenConstraint
  97. maxLenConstraint
  98. lenConstraint
  99. betweenLenConstraint
  100. minConstraint
  101. maxConstraint
  102. rangeConstraint
  103. regexConstraint
  104. )
  105. const (
  106. needOneData = minLenConstraint | maxLenConstraint | lenConstraint | minConstraint | maxConstraint | datetimeConstraint | regexConstraint
  107. needTwoData = betweenLenConstraint | rangeConstraint
  108. )
  109. // list of possible parameter and segment delimiter
  110. var (
  111. // slash has a special role, unlike the other parameters it must not be interpreted as a parameter
  112. routeDelimiter = []byte{slashDelimiter, '-', '.'}
  113. // list of greedy parameters
  114. greedyParameters = []byte{wildcardParam, plusParam}
  115. // list of chars for the parameter recognizing
  116. parameterStartChars = [256]bool{
  117. wildcardParam: true,
  118. plusParam: true,
  119. paramStarterChar: true,
  120. }
  121. // list of chars of delimiters and the starting parameter name char
  122. parameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...)
  123. // list of chars to find the end of a parameter
  124. parameterEndChars = [256]bool{
  125. optionalParam: true,
  126. paramStarterChar: true,
  127. escapeChar: true,
  128. slashDelimiter: true,
  129. '-': true,
  130. '.': true,
  131. }
  132. )
  133. // RoutePatternMatch reports whether path matches the provided Fiber route pattern.
  134. //
  135. // Patterns use the same syntax as routes registered on an App, including
  136. // parameters (for example `:id`), wildcards (`*`, `+`), and optional segments.
  137. // The optional Config argument can be used to control case sensitivity and
  138. // strict routing behavior. This helper allows checking potential matches
  139. // without registering a route.
  140. func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
  141. // See logic in (*Route).match and (*App).register
  142. var ctxParams [maxParams]string
  143. config := Config{}
  144. if len(cfg) > 0 {
  145. config = cfg[0]
  146. }
  147. if path == "" {
  148. path = "/"
  149. }
  150. // Cannot have an empty pattern
  151. if pattern == "" {
  152. pattern = "/"
  153. }
  154. // Pattern always start with a '/'
  155. if pattern[0] != '/' {
  156. pattern = "/" + pattern
  157. }
  158. patternPretty := []byte(pattern)
  159. // Case-sensitive routing, all to lowercase
  160. if !config.CaseSensitive {
  161. patternPretty = utilsbytes.UnsafeToLower(patternPretty)
  162. path = utilsstrings.ToLower(path)
  163. }
  164. // Strict routing, remove trailing slashes
  165. if !config.StrictRouting && len(patternPretty) > 1 {
  166. patternPretty = utils.TrimRight(patternPretty, '/')
  167. }
  168. parser, _ := routerParserPool.Get().(*routeParser) //nolint:errcheck // only contains routeParser
  169. parser.reset()
  170. patternStr := string(patternPretty)
  171. parser.parseRoute(patternStr)
  172. defer routerParserPool.Put(parser)
  173. // '*' wildcard matches any path
  174. if (patternStr == "/" && path == "/") || patternStr == "/*" {
  175. return true
  176. }
  177. // Does this route have parameters
  178. if len(parser.params) > 0 {
  179. if match := parser.getMatch(path, path, &ctxParams, false); match {
  180. return true
  181. }
  182. }
  183. // Check for a simple match
  184. patternPretty = RemoveEscapeCharBytes(patternPretty)
  185. return string(patternPretty) == path
  186. }
  187. func (parser *routeParser) reset() {
  188. parser.segs = parser.segs[:0]
  189. parser.params = parser.params[:0]
  190. parser.wildCardCount = 0
  191. parser.plusCount = 0
  192. }
  193. // parseRoute analyzes the route and divides it into segments for constant areas and parameters,
  194. // this information is needed later when assigning the requests to the declared routes
  195. func (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) {
  196. var n int
  197. var seg *routeSegment
  198. for pattern != "" {
  199. nextParamPosition := findNextParamPosition(pattern)
  200. // handle the parameter part
  201. if nextParamPosition == 0 {
  202. n, seg = parser.analyseParameterPart(pattern, customConstraints...)
  203. parser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg)
  204. } else {
  205. n, seg = parser.analyseConstantPart(pattern, nextParamPosition)
  206. parser.segs = append(parser.segs, seg)
  207. }
  208. pattern = pattern[n:]
  209. }
  210. // mark last segment
  211. if len(parser.segs) > 0 {
  212. parser.segs[len(parser.segs)-1].IsLast = true
  213. }
  214. parser.segs = addParameterMetaInfo(parser.segs)
  215. }
  216. // parseRoute analyzes the route and divides it into segments for constant areas and parameters,
  217. // this information is needed later when assigning the requests to the declared routes
  218. func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser {
  219. parser := routeParser{}
  220. parser.parseRoute(pattern, customConstraints...)
  221. // Check if the route has too many parameters
  222. if len(parser.params) > maxParams {
  223. panic(fmt.Sprintf("Route '%s' has %d parameters, which exceeds the maximum of %d",
  224. pattern, len(parser.params), maxParams))
  225. }
  226. return parser
  227. }
  228. // addParameterMetaInfo add important meta information to the parameter segments
  229. // to simplify the search for the end of the parameter
  230. func addParameterMetaInfo(segs []*routeSegment) []*routeSegment {
  231. var comparePart string
  232. segLen := len(segs)
  233. // loop from end to begin
  234. for i := segLen - 1; i >= 0; i-- {
  235. // set the compare part for the parameter
  236. if segs[i].IsParam {
  237. // important for finding the end of the parameter
  238. segs[i].ComparePart = RemoveEscapeChar(comparePart)
  239. } else {
  240. comparePart = segs[i].Const
  241. if len(comparePart) > 1 {
  242. comparePart = utils.TrimRight(comparePart, slashDelimiter)
  243. }
  244. }
  245. }
  246. // loop from beginning to end
  247. for i := range segLen {
  248. // check how often the compare part is in the following const parts
  249. if segs[i].IsParam {
  250. // check if parameter segments are directly after each other;
  251. // when neither this parameter nor the next parameter are greedy, we only want one character
  252. if segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy {
  253. segs[i].Length = 1
  254. }
  255. if segs[i].ComparePart == "" {
  256. continue
  257. }
  258. for j := i + 1; j <= len(segs)-1; j++ {
  259. if !segs[j].IsParam {
  260. // count is important for the greedy match
  261. segs[i].PartCount += strings.Count(segs[j].Const, segs[i].ComparePart)
  262. }
  263. }
  264. // check if the end of the segment is an optional slash and then if the segment is optional or the last one
  265. } else if segs[i].Const[len(segs[i].Const)-1] == slashDelimiter && (segs[i].IsLast || (segLen > i+1 && segs[i+1].IsOptional)) {
  266. segs[i].HasOptionalSlash = true
  267. }
  268. }
  269. return segs
  270. }
  271. // findNextParamPosition search for the next possible parameter start position
  272. func findNextParamPosition(pattern string) int {
  273. // Find the first parameter position
  274. next := -1
  275. for i := range pattern {
  276. if parameterStartChars[pattern[i]] && (i == 0 || pattern[i-1] != escapeChar) {
  277. next = i
  278. break
  279. }
  280. }
  281. if next > 0 && pattern[next] != wildcardParam {
  282. // checking the found parameterStartChar is a cluster
  283. for i := next + 1; i < len(pattern); i++ {
  284. if !parameterStartChars[pattern[i]] {
  285. return i - 1
  286. }
  287. }
  288. return len(pattern) - 1
  289. }
  290. return next
  291. }
  292. // analyseConstantPart find the end of the constant part and create the route segment
  293. func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (int, *routeSegment) {
  294. // handle the constant part
  295. processedPart := pattern
  296. if nextParamPosition != -1 {
  297. // remove the constant part until the parameter
  298. processedPart = pattern[:nextParamPosition]
  299. }
  300. constPart := RemoveEscapeChar(processedPart)
  301. return len(processedPart), &routeSegment{
  302. Const: constPart,
  303. Length: len(constPart),
  304. }
  305. }
  306. // analyseParameterPart find the parameter end and create the route segment
  307. func (parser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) {
  308. isWildCard := pattern[0] == wildcardParam
  309. isPlusParam := pattern[0] == plusParam
  310. paramEndPosition := 0
  311. paramConstraintStartPosition := -1
  312. paramConstraintEndPosition := -1
  313. // handle wildcard end
  314. if !isWildCard && !isPlusParam {
  315. paramEndPosition = -1
  316. search := pattern[1:]
  317. for i := range search {
  318. if paramConstraintStartPosition == -1 && search[i] == paramConstraintStart && (i == 0 || search[i-1] != escapeChar) {
  319. paramConstraintStartPosition = i + 1
  320. continue
  321. }
  322. if paramConstraintEndPosition == -1 && search[i] == paramConstraintEnd && (i == 0 || search[i-1] != escapeChar) {
  323. paramConstraintEndPosition = i + 1
  324. continue
  325. }
  326. if parameterEndChars[search[i]] {
  327. if (paramConstraintStartPosition == -1 && paramConstraintEndPosition == -1) ||
  328. (paramConstraintStartPosition != -1 && paramConstraintEndPosition != -1) {
  329. paramEndPosition = i
  330. break
  331. }
  332. }
  333. }
  334. switch {
  335. case paramEndPosition == -1:
  336. paramEndPosition = len(pattern) - 1
  337. case bytes.IndexByte(parameterDelimiterChars, pattern[paramEndPosition+1]) == -1:
  338. paramEndPosition++
  339. default:
  340. // do nothing
  341. }
  342. }
  343. // cut params part
  344. processedPart := pattern[0 : paramEndPosition+1]
  345. n := paramEndPosition + 1
  346. paramName := RemoveEscapeChar(GetTrimmedParam(processedPart))
  347. // Check has constraint
  348. var constraints []*Constraint
  349. if hasConstraint := paramConstraintStartPosition != -1 && paramConstraintEndPosition != -1; hasConstraint {
  350. constraintString := pattern[paramConstraintStartPosition+1 : paramConstraintEndPosition]
  351. userConstraints := splitNonEscaped(constraintString, paramConstraintSeparator)
  352. constraints = make([]*Constraint, 0, len(userConstraints))
  353. for _, c := range userConstraints {
  354. start := findNextNonEscapedCharPosition(c, paramConstraintDataStart)
  355. end := strings.LastIndexByte(c, paramConstraintDataEnd)
  356. // Assign constraint
  357. if start != -1 && end != -1 {
  358. constraint := &Constraint{
  359. ID: getParamConstraintType(c[:start]),
  360. Name: c[:start],
  361. customConstraints: customConstraints,
  362. }
  363. // remove escapes from data
  364. if constraint.ID != regexConstraint {
  365. constraint.Data = splitNonEscaped(c[start+1:end], paramConstraintDataSeparator)
  366. if len(constraint.Data) == 1 {
  367. constraint.Data[0] = RemoveEscapeChar(constraint.Data[0])
  368. } else if len(constraint.Data) == 2 { // This is fine, we simply expect two parts
  369. constraint.Data[0] = RemoveEscapeChar(constraint.Data[0])
  370. constraint.Data[1] = RemoveEscapeChar(constraint.Data[1])
  371. }
  372. }
  373. // Precompile regex if has regex constraint
  374. if constraint.ID == regexConstraint {
  375. constraint.Data = []string{c[start+1 : end]}
  376. constraint.RegexCompiler = regexp.MustCompile(constraint.Data[0])
  377. }
  378. constraints = append(constraints, constraint)
  379. } else {
  380. constraints = append(constraints, &Constraint{
  381. ID: getParamConstraintType(c),
  382. Data: []string{},
  383. Name: c,
  384. customConstraints: customConstraints,
  385. })
  386. }
  387. }
  388. paramName = RemoveEscapeChar(GetTrimmedParam(pattern[0:paramConstraintStartPosition]))
  389. }
  390. // add access iterator to wildcard and plus
  391. if isWildCard {
  392. parser.wildCardCount++
  393. paramName += strconv.Itoa(parser.wildCardCount)
  394. } else if isPlusParam {
  395. parser.plusCount++
  396. paramName += strconv.Itoa(parser.plusCount)
  397. }
  398. segment := &routeSegment{
  399. ParamName: paramName,
  400. IsParam: true,
  401. IsOptional: isWildCard || pattern[paramEndPosition] == optionalParam,
  402. IsGreedy: isWildCard || isPlusParam,
  403. }
  404. if len(constraints) > 0 {
  405. segment.Constraints = constraints
  406. }
  407. return n, segment
  408. }
  409. // findNextNonEscapedCharPosition searches the next char position and skips the escaped characters
  410. func findNextNonEscapedCharPosition(search string, char byte) int {
  411. for i := 0; i < len(search); i++ {
  412. if search[i] == char && (i == 0 || search[i-1] != escapeChar) {
  413. return i
  414. }
  415. }
  416. return -1
  417. }
  418. // splitNonEscaped slices s into all substrings separated by sep and returns a slice of the substrings between those separators
  419. // This function also takes a care of escape char when splitting.
  420. func splitNonEscaped(s string, sep byte) []string {
  421. var result []string
  422. i := findNextNonEscapedCharPosition(s, sep)
  423. for i > -1 {
  424. result = append(result, s[:i])
  425. s = s[i+1:]
  426. i = findNextNonEscapedCharPosition(s, sep)
  427. }
  428. return append(result, s)
  429. }
  430. func hasPartialMatchBoundary(path string, matchedLength int) bool {
  431. if matchedLength < 0 || matchedLength > len(path) {
  432. return false
  433. }
  434. if matchedLength == len(path) {
  435. return true
  436. }
  437. if matchedLength == 0 {
  438. return false
  439. }
  440. if path[matchedLength-1] == slashDelimiter {
  441. return true
  442. }
  443. if matchedLength < len(path) && path[matchedLength] == slashDelimiter {
  444. return true
  445. }
  446. return false
  447. }
  448. // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
  449. func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint:revive // Accepting a bool param is fine here
  450. originalDetectionPath := detectionPath
  451. var i, paramsIterator, partLen int
  452. for _, segment := range parser.segs {
  453. partLen = len(detectionPath)
  454. // check const segment
  455. if !segment.IsParam {
  456. i = segment.Length
  457. // is optional part or the const part must match with the given string
  458. // check if the end of the segment is an optional slash
  459. if segment.HasOptionalSlash && partLen == i-1 && detectionPath == segment.Const[:i-1] {
  460. i--
  461. } else if i > partLen || detectionPath[:i] != segment.Const {
  462. return false
  463. }
  464. } else {
  465. // determine parameter length
  466. i = findParamLen(detectionPath, segment)
  467. if !segment.IsOptional && i == 0 {
  468. return false
  469. }
  470. // take over the params positions
  471. params[paramsIterator] = path[:i]
  472. if !segment.IsOptional || i != 0 {
  473. // check constraint
  474. for _, c := range segment.Constraints {
  475. if matched := c.CheckConstraint(params[paramsIterator]); !matched {
  476. return false
  477. }
  478. }
  479. }
  480. paramsIterator++
  481. }
  482. // reduce founded part from the string
  483. if partLen > 0 {
  484. detectionPath, path = detectionPath[i:], path[i:]
  485. }
  486. }
  487. if detectionPath != "" {
  488. if !partialCheck {
  489. return false
  490. }
  491. consumedLength := len(originalDetectionPath) - len(detectionPath)
  492. if !hasPartialMatchBoundary(originalDetectionPath, consumedLength) {
  493. return false
  494. }
  495. }
  496. return true
  497. }
  498. // findParamLen for the expressjs wildcard behavior (right to left greedy)
  499. // look at the other segments and take what is left for the wildcard from right to left
  500. func findParamLen(s string, segment *routeSegment) int {
  501. if segment.IsLast {
  502. return findParamLenForLastSegment(s, segment)
  503. }
  504. if segment.Length != 0 && len(s) >= segment.Length {
  505. return segment.Length
  506. } else if segment.IsGreedy {
  507. // Search the parameters until the next constant part
  508. // special logic for greedy params
  509. searchCount := strings.Count(s, segment.ComparePart)
  510. if searchCount > 1 {
  511. return findGreedyParamLen(s, searchCount, segment)
  512. }
  513. }
  514. if len(segment.ComparePart) == 1 {
  515. if constPosition := strings.IndexByte(s, segment.ComparePart[0]); constPosition != -1 {
  516. return constPosition
  517. }
  518. } else if constPosition := strings.Index(s, segment.ComparePart); constPosition != -1 {
  519. // if the compare part was found, but contains a slash although this part is not greedy, then it must not match
  520. // example: /api/:param/fixedEnd -> path: /api/123/456/fixedEnd = no match , /api/123/fixedEnd = match
  521. if !segment.IsGreedy && strings.IndexByte(s[:constPosition], slashDelimiter) != -1 {
  522. return 0
  523. }
  524. return constPosition
  525. }
  526. return len(s)
  527. }
  528. // findParamLenForLastSegment get the length of the parameter if it is the last segment
  529. func findParamLenForLastSegment(s string, seg *routeSegment) int {
  530. if !seg.IsGreedy {
  531. if i := strings.IndexByte(s, slashDelimiter); i != -1 {
  532. return i
  533. }
  534. }
  535. return len(s)
  536. }
  537. // findGreedyParamLen get the length of the parameter for greedy segments from right to left
  538. func findGreedyParamLen(s string, searchCount int, segment *routeSegment) int {
  539. // check all from right to left segments
  540. for i := segment.PartCount; i > 0 && searchCount > 0; i-- {
  541. searchCount--
  542. constPosition := strings.LastIndex(s, segment.ComparePart)
  543. if constPosition == -1 {
  544. break
  545. }
  546. s = s[:constPosition]
  547. }
  548. return len(s)
  549. }
  550. // GetTrimmedParam trims the ':' & '?' from a string
  551. func GetTrimmedParam(param string) string {
  552. start := 0
  553. end := len(param)
  554. if end == 0 || param[start] != paramStarterChar { // is not a param
  555. return param
  556. }
  557. start++
  558. if param[end-1] == optionalParam { // is ?
  559. end--
  560. }
  561. return param[start:end]
  562. }
  563. // RemoveEscapeChar removes escape characters
  564. func RemoveEscapeChar(word string) string {
  565. // Fast path: check if there are any escape characters first
  566. escapeIdx := strings.IndexByte(word, '\\')
  567. if escapeIdx == -1 {
  568. return word // No escape chars, return original string without allocation
  569. }
  570. // Slow path: copy and remove escape characters
  571. b := []byte(word)
  572. dst := escapeIdx
  573. for src := escapeIdx + 1; src < len(b); src++ {
  574. if b[src] != '\\' {
  575. b[dst] = b[src]
  576. dst++
  577. }
  578. }
  579. return string(b[:dst])
  580. }
  581. // RemoveEscapeCharBytes removes escape characters
  582. func RemoveEscapeCharBytes(word []byte) []byte {
  583. dst := 0
  584. for src := range word {
  585. if word[src] != '\\' {
  586. word[dst] = word[src]
  587. dst++
  588. }
  589. }
  590. return word[:dst]
  591. }
  592. func getParamConstraintType(constraintPart string) TypeConstraint {
  593. switch constraintPart {
  594. case ConstraintInt:
  595. return intConstraint
  596. case ConstraintBool:
  597. return boolConstraint
  598. case ConstraintFloat:
  599. return floatConstraint
  600. case ConstraintAlpha:
  601. return alphaConstraint
  602. case ConstraintGUID:
  603. return guidConstraint
  604. case ConstraintMinLen, ConstraintMinLenLower:
  605. return minLenConstraint
  606. case ConstraintMaxLen, ConstraintMaxLenLower:
  607. return maxLenConstraint
  608. case ConstraintLen:
  609. return lenConstraint
  610. case ConstraintBetweenLen, ConstraintBetweenLenLower:
  611. return betweenLenConstraint
  612. case ConstraintMin:
  613. return minConstraint
  614. case ConstraintMax:
  615. return maxConstraint
  616. case ConstraintRange:
  617. return rangeConstraint
  618. case ConstraintDatetime:
  619. return datetimeConstraint
  620. case ConstraintRegex:
  621. return regexConstraint
  622. default:
  623. return noConstraint
  624. }
  625. }
  626. // CheckConstraint validates if a param matches the given constraint
  627. // Returns true if the param passes the constraint check, false otherwise
  628. func (c *Constraint) CheckConstraint(param string) bool {
  629. // First check if there's a custom constraint with the same name
  630. // This allows custom constraints to override built-in constraints
  631. for _, cc := range c.customConstraints {
  632. if cc.Name() == c.Name {
  633. return cc.Execute(param, c.Data...)
  634. }
  635. }
  636. var (
  637. err error
  638. num int
  639. )
  640. // Validate constraint has required data
  641. if c.ID&needOneData != 0 && len(c.Data) == 0 {
  642. return false
  643. }
  644. if c.ID&needTwoData != 0 && len(c.Data) < 2 {
  645. return false
  646. }
  647. switch c.ID {
  648. case noConstraint:
  649. return true
  650. case intConstraint:
  651. _, err = strconv.Atoi(param)
  652. case boolConstraint:
  653. _, err = strconv.ParseBool(param)
  654. case floatConstraint:
  655. _, err = strconv.ParseFloat(param, 32)
  656. case alphaConstraint:
  657. for _, r := range param {
  658. if !unicode.IsLetter(r) {
  659. return false
  660. }
  661. }
  662. case guidConstraint:
  663. _, err = uuid.Parse(param)
  664. case minLenConstraint:
  665. data, parseErr := strconv.Atoi(c.Data[0])
  666. if parseErr != nil {
  667. return false
  668. }
  669. if len(param) < data {
  670. return false
  671. }
  672. case maxLenConstraint:
  673. data, parseErr := strconv.Atoi(c.Data[0])
  674. if parseErr != nil {
  675. return false
  676. }
  677. if len(param) > data {
  678. return false
  679. }
  680. case lenConstraint:
  681. data, parseErr := strconv.Atoi(c.Data[0])
  682. if parseErr != nil {
  683. return false
  684. }
  685. if len(param) != data {
  686. return false
  687. }
  688. case betweenLenConstraint:
  689. data, parseErr := strconv.Atoi(c.Data[0])
  690. if parseErr != nil {
  691. return false
  692. }
  693. data2, parseErr := strconv.Atoi(c.Data[1])
  694. if parseErr != nil {
  695. return false
  696. }
  697. length := len(param)
  698. if length < data || length > data2 {
  699. return false
  700. }
  701. case minConstraint:
  702. data, parseErr := strconv.Atoi(c.Data[0])
  703. if parseErr != nil {
  704. return false
  705. }
  706. num, err = strconv.Atoi(param)
  707. if err != nil || num < data {
  708. return false
  709. }
  710. case maxConstraint:
  711. data, parseErr := strconv.Atoi(c.Data[0])
  712. if parseErr != nil {
  713. return false
  714. }
  715. num, err = strconv.Atoi(param)
  716. if err != nil || num > data {
  717. return false
  718. }
  719. case rangeConstraint:
  720. data, parseErr := strconv.Atoi(c.Data[0])
  721. if parseErr != nil {
  722. return false
  723. }
  724. data2, parseErr := strconv.Atoi(c.Data[1])
  725. if parseErr != nil {
  726. return false
  727. }
  728. num, err = strconv.Atoi(param)
  729. if err != nil || num < data || num > data2 {
  730. return false
  731. }
  732. case datetimeConstraint:
  733. _, err = time.Parse(c.Data[0], param)
  734. if err != nil {
  735. return false
  736. }
  737. case regexConstraint:
  738. if c.RegexCompiler == nil {
  739. return false
  740. }
  741. if match := c.RegexCompiler.MatchString(param); !match {
  742. return false
  743. }
  744. default:
  745. return false
  746. }
  747. return err == nil
  748. }