go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/web/route_tree_node.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package web 9 10 import ( 11 "strings" 12 "unicode" 13 "unicode/utf8" 14 ) 15 16 // RouteNodeType is a type of route node. 17 type RouteNodeType uint8 18 19 // RouteNodeTypes 20 const ( 21 RouteNodeTypeStatic RouteNodeType = iota // default 22 RouteNodeTypeRoot 23 RouteNodeTypeParam 24 RouteNodeTypeCatchAll 25 ) 26 27 // RouteNode is a node on the route tree. 28 type RouteNode struct { 29 RouteNodeType 30 31 Path string 32 IsWildcard bool 33 MaxParams uint8 34 Indices string 35 Children []*RouteNode 36 Route *Route 37 Priority uint32 38 } 39 40 // GetPath returns the node for a path, parameter values, and if there is a trailing slash redirect 41 // recommendation. 42 func (n *RouteNode) GetPath(path string) (route *Route, p RouteParameters, tsr bool) { 43 return n.getPath(path) 44 } 45 46 // incrementChildPriority increments priority of the given child and reorders if necessary 47 func (n *RouteNode) incrementChildPriority(index int) int { 48 n.Children[index].Priority++ 49 priority := n.Children[index].Priority 50 51 // adjust position (move to front) 52 newIndex := index 53 for newIndex > 0 && n.Children[newIndex-1].Priority < priority { 54 // swap node positions 55 temp := n.Children[newIndex-1] 56 n.Children[newIndex-1] = n.Children[newIndex] 57 n.Children[newIndex] = temp 58 newIndex-- 59 } 60 61 // build new index char string 62 if newIndex != index { 63 n.Indices = n.Indices[:newIndex] + // unchanged prefix, might be empty 64 n.Indices[index:index+1] + // the index char we move 65 n.Indices[newIndex:index] + n.Indices[index+1:] // rest without char at 'pos' 66 } 67 68 return newIndex 69 } 70 71 // AddChildRoute adds a node with the given handle to the path. 72 func (n *RouteNode) AddChildRoute(method, path string, handler Handler) { 73 fullPath := path 74 n.Priority++ 75 numParams := countParams(path) 76 77 // non-empty tree 78 if len(n.Path) > 0 || len(n.Children) > 0 { 79 walk: 80 for { 81 // Update maxParams of the current node 82 if numParams > n.MaxParams { 83 n.MaxParams = numParams 84 } 85 86 // Find the longest common prefix. 87 // This also implies that the common prefix contains no ':' or '*' 88 // since the existing key can't contain those chars. 89 i := 0 90 max := min(len(path), len(n.Path)) 91 for i < max && path[i] == n.Path[i] { 92 i++ 93 } 94 95 // Split edge 96 if i < len(n.Path) { 97 child := RouteNode{ 98 Path: n.Path[i:], 99 IsWildcard: n.IsWildcard, 100 RouteNodeType: RouteNodeTypeStatic, 101 Indices: n.Indices, 102 Children: n.Children, 103 Route: n.Route, 104 Priority: n.Priority - 1, 105 } 106 107 // Update maxParams (max of all Children) 108 for i := range child.Children { 109 if child.Children[i].MaxParams > child.MaxParams { 110 child.MaxParams = child.Children[i].MaxParams 111 } 112 } 113 114 n.Children = []*RouteNode{&child} 115 // []byte for proper unicode char conversion, see #65 116 n.Indices = string([]byte{n.Path[i]}) 117 n.Path = path[:i] 118 n.Route = nil 119 n.IsWildcard = false 120 } 121 122 // Make new node a child of this node 123 if i < len(path) { 124 path = path[i:] 125 126 if n.IsWildcard { 127 n = n.Children[0] 128 n.Priority++ 129 130 // Update maxParams of the child node 131 if numParams > n.MaxParams { 132 n.MaxParams = numParams 133 } 134 numParams-- 135 136 // Check if the wildcard matches 137 if len(path) >= len(n.Path) && n.Path == path[:len(n.Path)] { 138 // check for longer wildcard, e.g. :name and :names 139 if len(n.Path) >= len(path) || path[len(n.Path)] == '/' { 140 continue walk 141 } 142 } 143 144 panic("path segment '" + path + 145 "' conflicts with existing wildcard '" + n.Path + 146 "' in path '" + fullPath + "'") 147 } 148 149 c := path[0] 150 151 // slash after param 152 if n.RouteNodeType == RouteNodeTypeParam && c == '/' && len(n.Children) == 1 { 153 n = n.Children[0] 154 n.Priority++ 155 continue walk 156 } 157 158 // Check if a child with the next path byte exists 159 for i := 0; i < len(n.Indices); i++ { 160 if c == n.Indices[i] { 161 i = n.incrementChildPriority(i) 162 n = n.Children[i] 163 continue walk 164 } 165 } 166 167 // Otherwise insert it 168 if c != ':' && c != '*' { 169 // []byte for proper unicode char conversion, see #65 170 n.Indices += string([]byte{c}) 171 child := &RouteNode{ 172 MaxParams: numParams, 173 } 174 n.Children = append(n.Children, child) 175 n.incrementChildPriority(len(n.Indices) - 1) 176 n = child 177 } 178 n.insertChild(numParams, method, path, fullPath, handler) 179 return 180 } else if i == len(path) { // Make node a (in-path) leaf 181 if n.Route != nil { 182 panic("a handle is already registered for path '" + fullPath + "'") 183 } 184 n.Route = &Route{ 185 Handler: handler, 186 Path: fullPath, 187 Method: method, 188 } 189 } 190 return 191 } 192 } else { // Empty tree 193 n.insertChild(numParams, method, path, fullPath, handler) 194 n.RouteNodeType = RouteNodeTypeRoot 195 } 196 } 197 198 func (n *RouteNode) insertChild(numParams uint8, method, path, fullPath string, handler Handler) { 199 var offset int // already handled bytes of the path 200 201 // find prefix until first wildcard (beginning with ':'' or '*'') 202 for i, max := 0, len(path); numParams > 0; i++ { 203 c := path[i] 204 if c != ':' && c != '*' { 205 continue 206 } 207 208 // find wildcard end (either '/' or path end) 209 end := i + 1 210 for end < max && path[end] != '/' { 211 switch path[end] { 212 // the wildcard name must not contain ':' and '*' 213 case ':', '*': 214 panic("only one wildcard per path segment is allowed, has: '" + 215 path[i:] + "' in path '" + fullPath + "'") 216 default: 217 end++ 218 } 219 } 220 221 // check if this Node existing Children which would be 222 // unreachable if we insert the wildcard here 223 if len(n.Children) > 0 { 224 panic("wildcard route '" + path[i:end] + 225 "' conflicts with existing Children in path '" + fullPath + "'") 226 } 227 228 // check if the wildcard has a name 229 if end-i < 2 { 230 panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 231 } 232 233 if c == ':' { // param 234 // split path at the beginning of the wildcard 235 if i > 0 { 236 n.Path = path[offset:i] 237 offset = i 238 } 239 240 child := &RouteNode{ 241 RouteNodeType: RouteNodeTypeParam, 242 MaxParams: numParams, 243 } 244 n.Children = []*RouteNode{child} 245 n.IsWildcard = true 246 n = child 247 n.Priority++ 248 numParams-- 249 250 // if the path doesn't end with the wildcard, then there 251 // will be another non-wildcard subpath starting with '/' 252 if end < max { 253 n.Path = path[offset:end] 254 offset = end 255 256 child := &RouteNode{ 257 MaxParams: numParams, 258 Priority: 1, 259 } 260 n.Children = []*RouteNode{child} 261 n = child 262 } 263 } else { // catchAll 264 if end != max || numParams > 1 { 265 panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 266 } 267 268 if len(n.Path) > 0 && n.Path[len(n.Path)-1] == '/' { 269 panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 270 } 271 272 // currently fixed width 1 for '/' 273 i-- 274 if path[i] != '/' { 275 panic("no / before catch-all in path '" + fullPath + "'") 276 } 277 278 n.Path = path[offset:i] 279 280 // first node: catchAll node with empty path 281 child := &RouteNode{ 282 IsWildcard: true, 283 RouteNodeType: RouteNodeTypeCatchAll, 284 MaxParams: 1, 285 } 286 n.Children = []*RouteNode{child} 287 n.Indices = string(path[i]) 288 n = child 289 n.Priority++ 290 291 // second node: node holding the variable 292 child = &RouteNode{ 293 Path: path[i:], 294 RouteNodeType: RouteNodeTypeCatchAll, 295 MaxParams: 1, 296 Route: &Route{ 297 Handler: handler, 298 Path: fullPath, 299 Method: method, 300 }, 301 Priority: 1, 302 } 303 n.Children = []*RouteNode{child} 304 305 return 306 } 307 } 308 309 // insert remaining path part and handle to the leaf 310 n.Path = path[offset:] 311 n.Route = &Route{ 312 Handler: handler, 313 Path: fullPath, 314 Method: method, 315 } 316 } 317 318 // getPath Returns the handler registered with the given path (key). The values of 319 // wildcards are saved to a map. 320 // If no handle can be found, a TSR (trailing slash redirect) recommendation is 321 // made if a handle exists with an extra (without the) trailing slash for the 322 // given path. 323 func (n *RouteNode) getPath(path string) (route *Route, p RouteParameters, tsr bool) { 324 walk: // outer loop for walking the tree 325 for { 326 if len(path) > len(n.Path) { 327 if path[:len(n.Path)] == n.Path { 328 path = path[len(n.Path):] 329 // If this node does not have a wildcard (param or catchAll) 330 // child, we can just look up the next child node and continue 331 // to walk down the tree 332 if !n.IsWildcard { 333 c := path[0] 334 for i := 0; i < len(n.Indices); i++ { 335 if c == n.Indices[i] { 336 n = n.Children[i] 337 continue walk 338 } 339 } 340 341 // Nothing found. 342 // We can recommend to redirect to the same URL without a 343 // trailing slash if a leaf exists for that path. 344 tsr = (path == "/" && n.Route != nil) 345 return 346 } 347 348 // handle wildcard child 349 n = n.Children[0] 350 switch n.RouteNodeType { 351 case RouteNodeTypeParam: 352 // find param end (either '/' or path end) 353 end := 0 354 for end < len(path) && path[end] != '/' { 355 end++ 356 } 357 358 p = append(p, RouteParameter{ 359 Key: n.Path[1:], Value: path[:end], 360 }) 361 362 // we need to go deeper! 363 if end < len(path) { 364 if len(n.Children) > 0 { 365 path = path[end:] 366 n = n.Children[0] 367 continue walk 368 } 369 370 // ... but we can't 371 tsr = (len(path) == end+1) 372 return 373 } 374 375 if route = n.Route; route != nil { 376 return 377 } else if len(n.Children) == 1 { 378 // No handle found. Check if a handle for this path + a 379 // trailing slash exists for TSR recommendation 380 n = n.Children[0] 381 tsr = (n.Path == "/" && n.Route != nil) 382 } 383 384 return 385 386 case RouteNodeTypeCatchAll: 387 // translation note: 388 // was path[:] but the effect is the same 389 p = append(p, RouteParameter{ 390 Key: n.Path[2:], Value: path, 391 }) 392 393 route = n.Route 394 return 395 396 default: 397 panic("invalid node type") 398 } 399 } 400 } else if path == n.Path { 401 // We should have reached the node containing the handle. 402 // Check if this node has a handle registered. 403 if route = n.Route; route != nil { 404 return 405 } 406 407 if path == "/" && n.IsWildcard && n.RouteNodeType != RouteNodeTypeRoot { 408 tsr = true 409 return 410 } 411 412 // No handle found. Check if a handle for this path + a 413 // trailing slash exists for trailing slash recommendation 414 for i := 0; i < len(n.Indices); i++ { 415 if n.Indices[i] == '/' { 416 n = n.Children[i] 417 tsr = (len(n.Path) == 1 && n.Route != nil) || 418 (n.RouteNodeType == RouteNodeTypeCatchAll && n.Children[0].Route != nil) 419 return 420 } 421 } 422 423 return 424 } 425 426 // Nothing found. We can recommend to redirect to the same URL with an 427 // extra trailing slash if a leaf exists for that path 428 tsr = (path == "/") || 429 (len(n.Path) == len(path)+1 && n.Path[len(path)] == '/' && 430 path == n.Path[:len(n.Path)-1] && n.Route != nil) 431 return 432 } 433 } 434 435 // Makes a case-insensitive lookup of the given path and tries to find a handler. 436 // It can optionally also fix trailing slashes. 437 // It returns the case-corrected path and a bool indicating whether the lookup 438 // was successful. 439 func (n *RouteNode) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { 440 return n.findCaseInsensitivePathRec( 441 path, 442 strings.ToLower(path), 443 make([]byte, 0, len(path)+1), // preallocate enough memory for new path 444 [4]byte{}, // empty rune buffer 445 fixTrailingSlash, 446 ) 447 } 448 449 // shift bytes in array by n bytes left 450 func shiftNRuneBytes(rb [4]byte, n int) [4]byte { 451 switch n { 452 case 0: 453 return rb 454 case 1: 455 return [4]byte{rb[1], rb[2], rb[3], 0} 456 case 2: 457 return [4]byte{rb[2], rb[3]} 458 case 3: 459 return [4]byte{rb[3]} 460 default: 461 return [4]byte{} 462 } 463 } 464 465 // recursive case-insensitive lookup function used by n.findCaseInsensitivePath 466 func (n *RouteNode) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { 467 loNPath := strings.ToLower(n.Path) 468 469 walk: // outer loop for walking the tree 470 for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) { 471 // add common path to result 472 ciPath = append(ciPath, n.Path...) 473 474 if path = path[len(n.Path):]; len(path) > 0 { 475 loOld := loPath 476 loPath = loPath[len(loNPath):] 477 478 // If this node does not have a wildcard (param or catchAll) child, 479 // we can just look up the next child node and continue to walk down 480 // the tree 481 if !n.IsWildcard { 482 // skip rune bytes already processed 483 rb = shiftNRuneBytes(rb, len(loNPath)) 484 485 if rb[0] != 0 { 486 // old rune not finished 487 for i := 0; i < len(n.Indices); i++ { 488 if n.Indices[i] == rb[0] { 489 // continue with child node 490 n = n.Children[i] 491 loNPath = strings.ToLower(n.Path) 492 continue walk 493 } 494 } 495 } else { 496 // process a new rune 497 var rv rune 498 499 // find rune start 500 // runes are up to 4 byte long, 501 // -4 would definitely be another rune 502 var off int 503 for max := min(len(loNPath), 3); off < max; off++ { 504 if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) { 505 // read rune from cached lowercase path 506 rv, _ = utf8.DecodeRuneInString(loOld[i:]) 507 break 508 } 509 } 510 511 // calculate lowercase bytes of current rune 512 utf8.EncodeRune(rb[:], rv) 513 // skipp already processed bytes 514 rb = shiftNRuneBytes(rb, off) 515 516 for i := 0; i < len(n.Indices); i++ { 517 // lowercase matches 518 if n.Indices[i] == rb[0] { 519 // must use a recursive approach since both the 520 // uppercase byte and the lowercase byte might exist 521 // as an index 522 if out, found := n.Children[i].findCaseInsensitivePathRec( 523 path, loPath, ciPath, rb, fixTrailingSlash, 524 ); found { 525 return out, true 526 } 527 break 528 } 529 } 530 531 // same for uppercase rune, if it differs 532 if up := unicode.ToUpper(rv); up != rv { 533 utf8.EncodeRune(rb[:], up) 534 rb = shiftNRuneBytes(rb, off) 535 536 for i := 0; i < len(n.Indices); i++ { 537 // uppercase matches 538 if n.Indices[i] == rb[0] { 539 // continue with child node 540 n = n.Children[i] 541 loNPath = strings.ToLower(n.Path) 542 continue walk 543 } 544 } 545 } 546 } 547 548 // Nothing found. We can recommend to redirect to the same URL 549 // without a trailing slash if a leaf exists for that path 550 return ciPath, (fixTrailingSlash && path == "/" && n.Route != nil) 551 } 552 553 n = n.Children[0] 554 switch n.RouteNodeType { 555 case RouteNodeTypeParam: 556 // find param end (either '/' or path end) 557 k := 0 558 for k < len(path) && path[k] != '/' { 559 k++ 560 } 561 562 // add param value to case insensitive path 563 ciPath = append(ciPath, path[:k]...) 564 565 // we need to go deeper! 566 if k < len(path) { 567 if len(n.Children) > 0 { 568 // continue with child node 569 n = n.Children[0] 570 loNPath = strings.ToLower(n.Path) 571 loPath = loPath[k:] 572 path = path[k:] 573 continue 574 } 575 576 // ... but we can't 577 if fixTrailingSlash && len(path) == k+1 { 578 return ciPath, true 579 } 580 return ciPath, false 581 } 582 583 if n.Route != nil { 584 return ciPath, true 585 } else if fixTrailingSlash && len(n.Children) == 1 { 586 // No handle found. Check if a handle for this path + a 587 // trailing slash exists 588 n = n.Children[0] 589 if n.Path == "/" && n.Route != nil { 590 return append(ciPath, '/'), true 591 } 592 } 593 return ciPath, false 594 595 case RouteNodeTypeCatchAll: 596 return append(ciPath, path...), true 597 598 default: 599 panic("invalid node type") 600 } 601 } else { 602 // We should have reached the node containing the handle. 603 // Check if this node has a handle registered. 604 if n.Route != nil { 605 return ciPath, true 606 } 607 608 // No handle found. 609 // Try to fix the path by adding a trailing slash 610 if fixTrailingSlash { 611 for i := 0; i < len(n.Indices); i++ { 612 if n.Indices[i] == '/' { 613 n = n.Children[i] 614 if (len(n.Path) == 1 && n.Route != nil) || 615 (n.RouteNodeType == RouteNodeTypeCatchAll && n.Children[0].Route != nil) { 616 return append(ciPath, '/'), true 617 } 618 return ciPath, false 619 } 620 } 621 } 622 return ciPath, false 623 } 624 } 625 626 // Nothing found. 627 // Try to fix the path by adding / removing a trailing slash 628 if fixTrailingSlash { 629 if path == "/" { 630 return ciPath, true 631 } 632 if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' && 633 loPath[1:] == loNPath[1:len(loPath)] && n.Route != nil { 634 return append(ciPath, n.Path...), true 635 } 636 } 637 return ciPath, false 638 } 639 640 // CleanPath is the URL version of path.Clean, it returns a canonical URL path 641 // for p, eliminating . and .. elements. 642 // 643 // The following rules are applied iteratively until no further processing can 644 // be done: 645 // 1. Replace multiple slashes with a single slash. 646 // 2. Eliminate each . path name element (the current directory). 647 // 3. Eliminate each inner .. path name element (the parent directory) 648 // along with the non-.. element that precedes it. 649 // 4. Eliminate .. elements that begin a rooted path: 650 // that is, replace "/.." by "/" at the beginning of a path. 651 // 652 // If the result of this process is an empty string, "/" is returned 653 func CleanPath(p string) string { 654 // Turn empty string into "/" 655 if p == "" { 656 return "/" 657 } 658 659 n := len(p) 660 var buf []byte 661 662 // Invariants: 663 // reading from path; r is index of next byte to process. 664 // writing to buf; w is index of next byte to write. 665 666 // path must start with '/' 667 r := 1 668 w := 1 669 670 if p[0] != '/' { 671 r = 0 672 buf = make([]byte, n+1) 673 buf[0] = '/' 674 } 675 676 trailing := n > 2 && p[n-1] == '/' 677 678 // A bit more clunky without a 'lazybuf' like the path package, but the loop 679 // gets completely inlined (bufApp). So in contrast to the path package this 680 // loop has no expensive function calls (except 1x make) 681 682 for r < n { 683 switch { 684 case p[r] == '/': 685 // empty path element, trailing slash is added after the end 686 r++ 687 688 case p[r] == '.' && r+1 == n: 689 trailing = true 690 r++ 691 692 case p[r] == '.' && p[r+1] == '/': 693 // . element 694 r++ 695 696 case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 697 // .. element: remove to last / 698 r += 2 699 700 if w > 1 { 701 // can backtrack 702 w-- 703 704 if buf == nil { 705 for w > 1 && p[w] != '/' { 706 w-- 707 } 708 } else { 709 for w > 1 && buf[w] != '/' { 710 w-- 711 } 712 } 713 } 714 715 default: 716 // real path element. 717 // add slash if needed 718 if w > 1 { 719 bufApp(&buf, p, w, '/') 720 w++ 721 } 722 723 // copy element 724 for r < n && p[r] != '/' { 725 bufApp(&buf, p, w, p[r]) 726 w++ 727 r++ 728 } 729 } 730 } 731 732 // re-append trailing slash 733 if trailing && w > 1 { 734 bufApp(&buf, p, w, '/') 735 w++ 736 } 737 738 if buf == nil { 739 return p[:w] 740 } 741 return string(buf[:w]) 742 } 743 744 // internal helper to lazily create a buffer if necessary 745 func bufApp(buf *[]byte, s string, w int, c byte) { 746 if *buf == nil { 747 if s[w] == c { 748 return 749 } 750 751 *buf = make([]byte, len(s)) 752 copy(*buf, s[:w]) 753 } 754 (*buf)[w] = c 755 } 756 757 func countParams(path string) uint8 { 758 var n uint 759 for i := 0; i < len(path); i++ { 760 if path[i] != ':' && path[i] != '*' { 761 continue 762 } 763 n++ 764 } 765 if n >= 255 { 766 return 255 767 } 768 return uint8(n) 769 } 770 771 func min(a, b int) int { 772 if a <= b { 773 return a 774 } 775 return b 776 }