package httptreemux import ( "fmt" "net/url" "strings" ) type node struct { path string priority int // The list of static children to check. staticIndices []byte staticChild []*node // If none of the above match, check the wildcard children wildcardChild *node // If none of the above match, then we use the catch-all, if applicable. catchAllChild *node // Data for the node is below. addSlash bool isCatchAll bool // If true, the head handler was set implicitly, so let it also be set explicitly. implicitHead bool // If this node is the end of the URL, then call the handler, if applicable. leafHandler map[string]HandlerFunc // The names of the parameters to apply. leafWildcardNames []string } func (n *node) sortStaticChild(i int) { for i > 0 && n.staticChild[i].priority > n.staticChild[i-1].priority { n.staticChild[i], n.staticChild[i-1] = n.staticChild[i-1], n.staticChild[i] n.staticIndices[i], n.staticIndices[i-1] = n.staticIndices[i-1], n.staticIndices[i] i -= 1 } } func (n *node) setHandler(verb string, handler HandlerFunc, implicitHead bool) { if n.leafHandler == nil { n.leafHandler = make(map[string]HandlerFunc) } _, ok := n.leafHandler[verb] if ok && (verb != "HEAD" || !n.implicitHead) { panic(fmt.Sprintf("%s already handles %s", n.path, verb)) } n.leafHandler[verb] = handler if verb == "HEAD" { n.implicitHead = implicitHead } } func (n *node) addPath(path string, wildcards []string, inStaticToken bool) *node { leaf := len(path) == 0 if leaf { if wildcards != nil { // Make sure the current wildcards are the same as the old ones. // If not then we have an ambiguous path. if n.leafWildcardNames != nil { if len(n.leafWildcardNames) != len(wildcards) { // This should never happen. panic("Reached leaf node with differing wildcard array length. Please report this as a bug.") } for i := 0; i < len(wildcards); i++ { if n.leafWildcardNames[i] != wildcards[i] { panic(fmt.Sprintf("Wildcards %v are ambiguous with wildcards %v", n.leafWildcardNames, wildcards)) } } } else { // No wildcards yet, so just add the existing set. n.leafWildcardNames = wildcards } } return n } c := path[0] nextSlash := strings.Index(path, "/") var thisToken string var tokenEnd int if c == '/' { // Done processing the previous token, so reset inStaticToken to false. thisToken = "/" tokenEnd = 1 } else if nextSlash == -1 { thisToken = path tokenEnd = len(path) } else { thisToken = path[0:nextSlash] tokenEnd = nextSlash } remainingPath := path[tokenEnd:] if c == '*' && !inStaticToken { // Token starts with a *, so it's a catch-all thisToken = thisToken[1:] if n.catchAllChild == nil { n.catchAllChild = &node{path: thisToken, isCatchAll: true} } if path[1:] != n.catchAllChild.path { panic(fmt.Sprintf("Catch-all name in %s doesn't match %s. You probably tried to define overlapping catchalls", path, n.catchAllChild.path)) } if nextSlash != -1 { panic("/ after catch-all found in " + path) } if wildcards == nil { wildcards = []string{thisToken} } else { wildcards = append(wildcards, thisToken) } n.catchAllChild.leafWildcardNames = wildcards return n.catchAllChild } else if c == ':' && !inStaticToken { // Token starts with a : thisToken = thisToken[1:] if wildcards == nil { wildcards = []string{thisToken} } else { wildcards = append(wildcards, thisToken) } if n.wildcardChild == nil { n.wildcardChild = &node{path: "wildcard"} } return n.wildcardChild.addPath(remainingPath, wildcards, false) } else { // if strings.ContainsAny(thisToken, ":*") { // panic("* or : in middle of path component " + path) // } unescaped := false if len(thisToken) >= 2 && !inStaticToken { if thisToken[0] == '\\' && (thisToken[1] == '*' || thisToken[1] == ':' || thisToken[1] == '\\') { // The token starts with a character escaped by a backslash. Drop the backslash. c = thisToken[1] thisToken = thisToken[1:] unescaped = true } } // Set inStaticToken to ensure that the rest of this token is not mistaken // for a wildcard if a prefix split occurs at a '*' or ':'. inStaticToken = (c != '/') // Do we have an existing node that starts with the same letter? for i, index := range n.staticIndices { if c == index { // Yes. Split it based on the common prefix of the existing // node and the new one. child, prefixSplit := n.splitCommonPrefix(i, thisToken) child.priority++ n.sortStaticChild(i) if unescaped { // Account for the removed backslash. prefixSplit++ } return child.addPath(path[prefixSplit:], wildcards, inStaticToken) } } // No existing node starting with this letter, so create it. child := &node{path: thisToken} if n.staticIndices == nil { n.staticIndices = []byte{c} n.staticChild = []*node{child} } else { n.staticIndices = append(n.staticIndices, c) n.staticChild = append(n.staticChild, child) } return child.addPath(remainingPath, wildcards, inStaticToken) } } func (n *node) splitCommonPrefix(existingNodeIndex int, path string) (*node, int) { childNode := n.staticChild[existingNodeIndex] if strings.HasPrefix(path, childNode.path) { // No split needs to be done. Rather, the new path shares the entire // prefix with the existing node, so the new node is just a child of // the existing one. Or the new path is the same as the existing path, // which means that we just move on to the next token. Either way, // this return accomplishes that return childNode, len(childNode.path) } var i int // Find the length of the common prefix of the child node and the new path. for i = range childNode.path { if i == len(path) { break } if path[i] != childNode.path[i] { break } } commonPrefix := path[0:i] childNode.path = childNode.path[i:] // Create a new intermediary node in the place of the existing node, with // the existing node as a child. newNode := &node{ path: commonPrefix, priority: childNode.priority, // Index is the first letter of the non-common part of the path. staticIndices: []byte{childNode.path[0]}, staticChild: []*node{childNode}, } n.staticChild[existingNodeIndex] = newNode return newNode, i } func (n *node) search(method, path string) (found *node, handler HandlerFunc, params []string) { // if test != nil { // test.Logf("Searching for %s in %s", path, n.dumpTree("", "")) // } pathLen := len(path) if pathLen == 0 { if len(n.leafHandler) == 0 { return nil, nil, nil } else { return n, n.leafHandler[method], nil } } // First see if this matches a static token. firstChar := path[0] for i, staticIndex := range n.staticIndices { if staticIndex == firstChar { child := n.staticChild[i] childPathLen := len(child.path) if pathLen >= childPathLen && child.path == path[:childPathLen] { nextPath := path[childPathLen:] found, handler, params = child.search(method, nextPath) } break } } // If we found a node and it had a valid handler, then return here. Otherwise // let's remember that we found this one, but look for a better match. if handler != nil { return } if n.wildcardChild != nil { // Didn't find a static token, so check for a wildcard. nextSlash := strings.IndexByte(path, '/') if nextSlash < 0 { nextSlash = pathLen } thisToken := path[0:nextSlash] nextToken := path[nextSlash:] if len(thisToken) > 0 { // Don't match on empty tokens. wcNode, wcHandler, wcParams := n.wildcardChild.search(method, nextToken) if wcHandler != nil || (found == nil && wcNode != nil) { unescaped, err := url.QueryUnescape(thisToken) if err != nil { unescaped = thisToken } if wcParams == nil { wcParams = []string{unescaped} } else { wcParams = append(wcParams, unescaped) } if wcHandler != nil { return wcNode, wcHandler, wcParams } // Didn't actually find a handler here, so remember that we // found a node but also see if we can fall through to the // catchall. found = wcNode handler = wcHandler params = wcParams } } } catchAllChild := n.catchAllChild if catchAllChild != nil { // Hit the catchall, so just assign the whole remaining path if it // has a matching handler. handler = catchAllChild.leafHandler[method] // Found a handler, or we found a catchall node without a handler. // Either way, return it since there's nothing left to check after this. if handler != nil || found == nil { unescaped, err := url.QueryUnescape(path) if err != nil { unescaped = path } return catchAllChild, handler, []string{unescaped} } } return found, handler, params } func (n *node) dumpTree(prefix, nodeType string) string { line := fmt.Sprintf("%s %02d %s%s [%d] %v wildcards %v\n", prefix, n.priority, nodeType, n.path, len(n.staticChild), n.leafHandler, n.leafWildcardNames) prefix += " " for _, node := range n.staticChild { line += node.dumpTree(prefix, "") } if n.wildcardChild != nil { line += n.wildcardChild.dumpTree(prefix, ":") } if n.catchAllChild != nil { line += n.catchAllChild.dumpTree(prefix, "*") } return line }