path.go 25 KB

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