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