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  }