@@ -48,12 +48,16 @@ pub(crate) enum NodeType {
4848
4949 /// A route parameter, e.g. '/{id}'.
5050 ///
51- /// The leaves of a parameter node are suffixes within
52- /// the segment, i.e. before the next '/', sorted by length.
53- /// This allows for a reverse linear search to determine the
54- /// correct leaf. It would also be possible to use a reverse
55- /// prefix-tree here, but is likely not worth the complexity.
56- Param ,
51+ /// If `suffix` is `false`, the only child of this node is
52+ /// a static '/', allowing for a fast path when searching.
53+ /// Otherwise, the route may have static suffixes, e.g. '/{id}.png'.
54+ ///
55+ /// The leaves of a parameter node are the static suffixes
56+ /// sorted by length. This allows for a reverse linear search
57+ /// to determine the correct leaf. It would also be possible to
58+ /// use a reverse prefix-tree here, but is likely not worth the
59+ /// complexity.
60+ Param { suffix : bool } ,
5761
5862 /// A catch-all parameter, e.g. '/{*file}'.
5963 CatchAll ,
@@ -144,7 +148,7 @@ impl<T> Node<T> {
144148
145149 // For parameters with a suffix, we have to find the matching suffix or
146150 // create a new child node.
147- if current. node_type == NodeType :: Param {
151+ if matches ! ( current. node_type, NodeType :: Param { .. } ) {
148152 let terminator = remaining
149153 . iter ( )
150154 . position ( |& b| b == b'/' )
@@ -169,14 +173,13 @@ impl<T> Node<T> {
169173 }
170174
171175 // If there is no matching suffix, create a new suffix node.
172- let child = Node {
176+ let child = current . add_suffix_child ( Node {
173177 prefix : suffix. to_owned ( ) ,
174178 node_type : NodeType :: Static ,
175179 priority : 1 ,
176180 ..Node :: default ( )
177- } ;
178-
179- let child = current. add_suffix_child ( child) ;
181+ } ) ;
182+ current. node_type = NodeType :: Param { suffix : true } ;
180183 current = & mut current. children [ child] ;
181184
182185 // If this is the final route segment, insert the value.
@@ -477,8 +480,7 @@ impl<T> Node<T> {
477480 }
478481
479482 // Otherwise, we're inserting a regular route parameter.
480- assert_eq ! ( prefix[ wildcard. clone( ) ] [ 0 ] , b'{' ) ;
481-
483+ //
482484 // Add the prefix before the wildcard into the current node.
483485 if wildcard. start > 0 {
484486 current. prefix = prefix. slice_until ( wildcard. start ) . to_owned ( ) ;
@@ -503,17 +505,25 @@ impl<T> Node<T> {
503505 }
504506
505507 // Add the parameter as a child node.
508+ let has_suffix = !matches ! ( * suffix, b"" | b"/" ) ;
506509 let child = current. add_child ( Node {
507510 priority : 1 ,
508- node_type : NodeType :: Param ,
511+ node_type : NodeType :: Param { suffix : has_suffix } ,
509512 prefix : wildcard. to_owned ( ) ,
510513 ..Node :: default ( )
511514 } ) ;
512515
513516 current. wild_child = true ;
514517 current = & mut current. children [ child] ;
515518
516- // Add the static suffix before the '/', if there is one.
519+ // Add the static suffix until the '/', if there is one.
520+ //
521+ // Note that for '/' suffixes where `suffix: false`, this
522+ // unconditionally introduces an extra node for the '/'
523+ // without attempting to merge with the remaining route.
524+ // This makes converting a non-suffix parameter node into
525+ // a suffix one easier during insertion, but slightly hurts
526+ // performance.
517527 if !suffix. is_empty ( ) {
518528 let child = current. add_suffix_child ( Node {
519529 priority : 1 ,
@@ -534,7 +544,6 @@ impl<T> Node<T> {
534544 // If there is a static segment after the '/', setup the node
535545 // for the rest of the route.
536546 if prefix[ 0 ] != b'{' || prefix. is_escaped ( 0 ) {
537- assert ! ( prefix[ 0 ] != b'{' ) ;
538547 current. indices . push ( prefix[ 0 ] ) ;
539548 let child = current. add_child ( Node {
540549 priority : 1 ,
@@ -638,7 +647,55 @@ impl<T> Node<T> {
638647 // Continue searching in the wildcard child, which is kept at the end of the list.
639648 node = node. children . last ( ) . unwrap ( ) ;
640649 match node. node_type {
641- NodeType :: Param => {
650+ NodeType :: Param { suffix : false } => {
651+ // Check for more path segments.
652+ let terminator = match path. iter ( ) . position ( |& c| c == b'/' ) {
653+ // Double `//` implying an empty parameter, no match.
654+ Some ( 0 ) => break ' walk,
655+
656+ // Found another segment.
657+ Some ( i) => i,
658+
659+ // This is the last path segment.
660+ None => {
661+ // If this is the last path segment and there is a matching
662+ // value without a suffix, we have a match.
663+ let Some ( ref value) = node. value else {
664+ break ' walk;
665+ } ;
666+
667+ // Store the parameter value.
668+ // Parameters are normalized so the key is irrelevant for now.
669+ params. push ( b"" , path) ;
670+
671+ // Remap the keys of any route parameters we accumulated during the
672+ params
673+ . for_each_key_mut ( |( i, param) | param. key = & node. remapping [ i] ) ;
674+
675+ return Ok ( ( value, params) ) ;
676+ }
677+ } ;
678+
679+ // Found another path segment.
680+ let ( param, rest) = path. split_at ( terminator) ;
681+
682+ // If there is a static child, continue the search.
683+ let [ child] = node. children . as_slice ( ) else {
684+ break ' walk;
685+ } ;
686+
687+ // Store the parameter value.
688+ // Parameters are normalized so the key is irrelevant for now.
689+ params. push ( b"" , param) ;
690+
691+ // Continue searching.
692+ path = rest;
693+ node = child;
694+ backtracking = false ;
695+ continue ' walk;
696+ }
697+
698+ NodeType :: Param { suffix : true } => {
642699 // Check for more path segments.
643700 let slash = path. iter ( ) . position ( |& c| c == b'/' ) ;
644701 let terminator = match slash {
0 commit comments