@@ -38,6 +38,9 @@ pub(crate) enum NodeType {
3838 Root ,
3939 /// A route parameter, e.g. `/{id}`.
4040 Param ,
41+ /// A route parameter that is followed by a static suffix
42+ /// before a trailing slash, ex: `/{id}.png`.
43+ ParamSuffix { suffix_start : usize } ,
4144 /// A catch-all parameter, e.g. `/*file`.
4245 CatchAll ,
4346 /// A static prefix, e.g. `/foo`.
@@ -127,7 +130,11 @@ impl<T> Node<T> {
127130 // After matching against a wildcard the next character is always `/`.
128131 //
129132 // Continue searching in the child node if it already exists.
130- if current. node_type == NodeType :: Param && current. children . len ( ) == 1 {
133+ if matches ! (
134+ current. node_type,
135+ NodeType :: Param | NodeType :: ParamSuffix { .. }
136+ ) && current. children . len ( ) == 1
137+ {
131138 debug_assert_eq ! ( next, b'/' ) ;
132139 current = & mut current. children [ 0 ] ;
133140 current. priority += 1 ;
@@ -173,11 +180,14 @@ impl<T> Node<T> {
173180 current = current. children . last_mut ( ) . unwrap ( ) ;
174181 current. priority += 1 ;
175182
183+ let segment = remaining
184+ . iter ( )
185+ . position ( |b| * b == b'/' )
186+ . unwrap_or ( remaining. len ( ) ) ;
187+
176188 // Make sure the route parameter matches.
177- if let Some ( wildcard) = remaining. get ( ..current. prefix . len ( ) ) {
178- if * wildcard != * current. prefix {
179- return Err ( InsertError :: conflict ( & route, remaining, current) ) ;
180- }
189+ if remaining[ ..segment] != * current. prefix {
190+ return Err ( InsertError :: conflict ( & route, remaining, current) ) ;
181191 }
182192
183193 // Catch-all routes cannot have children.
@@ -402,10 +412,39 @@ impl<T> Node<T> {
402412 prefix = prefix. slice_off ( wildcard. start ) ;
403413 }
404414
415+ let ( node_type, wildcard) = match prefix. get ( wildcard. len ( ) ) {
416+ // The entire route segment consists of the wildcard.
417+ None | Some ( & b'/' ) => {
418+ let wildcard_prefix = prefix. slice_until ( wildcard. len ( ) ) ;
419+ prefix = prefix. slice_off ( wildcard_prefix. len ( ) ) ;
420+ ( NodeType :: Param , wildcard_prefix)
421+ }
422+ // The route parameter is followed a static suffix within the current segment.
423+ _ => {
424+ let end = prefix
425+ . iter ( )
426+ . position ( |& b| b == b'/' )
427+ . unwrap_or ( prefix. len ( ) ) ;
428+
429+ let wildcard_prefix = prefix. slice_until ( end) ;
430+ let suffix = wildcard_prefix. slice_off ( wildcard. len ( ) ) ;
431+
432+ // Multiple parameters within the same segment, e.g. `/{foo}{bar}`.
433+ if matches ! ( find_wildcard( suffix) , Ok ( Some ( _) ) ) {
434+ return Err ( InsertError :: InvalidParamSegment ) ;
435+ }
436+
437+ prefix = prefix. slice_off ( end) ;
438+
439+ let suffix_start = wildcard. len ( ) ;
440+ ( NodeType :: ParamSuffix { suffix_start } , wildcard_prefix)
441+ }
442+ } ;
443+
405444 // Add the parameter as a child node.
406445 let child = Self {
407- node_type : NodeType :: Param ,
408- prefix : prefix . slice_until ( wildcard. len ( ) ) . to_owned ( ) ,
446+ node_type,
447+ prefix : wildcard. to_owned ( ) ,
409448 ..Self :: default ( )
410449 } ;
411450
@@ -415,8 +454,7 @@ impl<T> Node<T> {
415454 current. priority += 1 ;
416455
417456 // If the route doesn't end in the wildcard, we have to insert the suffix as a child.
418- if wildcard. len ( ) < prefix. len ( ) {
419- prefix = prefix. slice_off ( wildcard. len ( ) ) ;
457+ if !prefix. is_empty ( ) {
420458 let child = Self {
421459 priority : 1 ,
422460 ..Self :: default ( )
@@ -616,6 +654,75 @@ impl<T> Node<T> {
616654 // Otherwise, there are no matching routes in the tree.
617655 return Err ( MatchError :: NotFound ) ;
618656 }
657+ NodeType :: ParamSuffix { suffix_start } => {
658+ let suffix = & current. prefix [ suffix_start..] ;
659+
660+ // Check for more path segments.
661+ let end = match path. iter ( ) . position ( |& c| c == b'/' ) {
662+ // Double `//` implying an empty parameter, no match.
663+ Some ( 0 ) => {
664+ try_backtrack ! ( ) ;
665+ return Err ( MatchError :: NotFound ) ;
666+ }
667+ // Found another segment.
668+ Some ( i) => i,
669+ // This is the last path segment.
670+ None => path. len ( ) ,
671+ } ;
672+
673+ // The path cannot contain a non-empty parameter and the suffix.
674+ if suffix. len ( ) >= end {
675+ try_backtrack ! ( ) ;
676+ return Err ( MatchError :: NotFound ) ;
677+ }
678+
679+ // Ensure the suffix matches.
680+ for ( a, b) in path[ ..end] . iter ( ) . rev ( ) . zip ( suffix. iter ( ) . rev ( ) ) {
681+ if a != b {
682+ try_backtrack ! ( ) ;
683+ return Err ( MatchError :: NotFound ) ;
684+ }
685+ }
686+
687+ let param = & path[ ..end - suffix. len ( ) ] ;
688+ let rest = & path[ end..] ;
689+
690+ if rest. is_empty ( ) {
691+ let value = match current. value {
692+ // Found the matching value.
693+ Some ( ref value) => value,
694+ // Otherwise, this route does not match.
695+ None => {
696+ try_backtrack ! ( ) ;
697+ return Err ( MatchError :: NotFound ) ;
698+ }
699+ } ;
700+
701+ // Store the parameter value.
702+ params. push ( b"" , param) ;
703+
704+ // Remap the keys of any route parameters we accumulated during the search.
705+ params. for_each_key_mut ( |( i, key) | * key = & current. remapping [ i] ) ;
706+
707+ return Ok ( ( value, params) ) ;
708+ }
709+
710+ // If there is a static child, continue the search.
711+ if let [ child] = current. children . as_slice ( ) {
712+ // Store the parameter value.
713+ params. push ( b"" , param) ;
714+
715+ // Continue searching.
716+ path = rest;
717+ current = child;
718+ backtracking = false ;
719+ continue ' walk;
720+ }
721+
722+ // Otherwise, this route does not match.
723+ try_backtrack ! ( ) ;
724+ return Err ( MatchError :: NotFound ) ;
725+ }
619726 NodeType :: CatchAll => {
620727 // Catch-all segments are only allowed at the end of the route, meaning
621728 // this node must contain the value.
@@ -785,13 +892,6 @@ fn find_wildcard(path: UnescapedRef<'_>) -> Result<Option<Range<usize>>, InsertE
785892 return Err ( InsertError :: InvalidParam ) ;
786893 }
787894
788- if let Some ( & c) = path. get ( i + 1 ) {
789- // Prefixes after route parameters are not supported.
790- if c != b'/' {
791- return Err ( InsertError :: InvalidParamSegment ) ;
792- }
793- }
794-
795895 return Ok ( Some ( start..i + 1 ) ) ;
796896 }
797897 // `*` and `/` are invalid in parameter names.
0 commit comments