Repository: actix/actix-net (language: rust)

Issue:
dynamic prefixes should not be able match without a slash seperator
> Good! Now we have to find a solution for dynamic prefixes. One idea is to implement it in regex, where each dynamic prefix pattern is put inside a capture group with a pattern to detect segment boundary: `(original_regex_pattern)[/$]`. But this may add complexity. I'm not sure.

_Originally posted by @aliemjay in https://github.com/actix/actix-net/pull/373#discussion_r672394311_

test case that should not work but does:
```rust
let resource = ResourceDef::prefix(r"/id/{id:\d{3}}");
assert!(resource.is_match("/id/1234"));
assert_eq!(resource.find_match("/id/1234"), Some(7));
```


Discussion / hints:

Fix patch:
diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs
index a3ea8a1c7f..5c98493db1 100644
--- a/actix-router/src/resource.rs
+++ b/actix-router/src/resource.rs
@@ -572,6 +572,20 @@ impl ResourceDef {
             PatternType::Prefix(ref prefix) if prefix == path => true,
             PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path),
 
+            // dynamic prefix
+            PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => {
+                match re.find(path) {
+                    // prefix matches exactly
+                    Some(m) if m.end() == path.len() => true,
+
+                    // prefix matches part
+                    Some(m) => is_strict_prefix(m.as_str(), path),
+
+                    // prefix does not match
+                    None => false,
+                }
+            }
+
             PatternType::Dynamic(ref re, _) => re.is_match(path),
             PatternType::DynamicSet(ref re, _) => re.is_match(path),
         }
@@ -623,6 +637,20 @@ impl ResourceDef {
             PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()),
             PatternType::Prefix(_) => None,
 
+            // dynamic prefix
+            PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => {
+                match re.find(path) {
+                    // prefix matches exactly
+                    Some(m) if m.end() == path.len() => Some(m.end()),
+
+                    // prefix matches part
+                    Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()),
+
+                    // prefix does not match
+                    _ => None,
+                }
+            }
+
             PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()),
 
             PatternType::DynamicSet(re, params) => {
@@ -654,7 +682,7 @@ impl ResourceDef {
     /// assert_eq!(path.unprocessed(), "");
     /// ```
     pub fn capture_match_info<T: ResourcePath>(&self, path: &mut Path<T>) -> bool {
-        profile_method!(is_path_match);
+        profile_method!(capture_match_info);
         self.capture_match_info_fn(path, |_, _| true, ())
     }
 
@@ -707,7 +735,7 @@ impl ResourceDef {
         T: ResourcePath,
         F: FnOnce(&R, U) -> bool,
     {
-        profile_method!(is_path_match_fn);
+        profile_method!(capture_match_info_fn);
 
         let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
         let path = resource.resource_path();
@@ -1475,10 +1503,6 @@ mod tests {
         assert_eq!(&path[0], "test2");
         assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html");
 
-        let resource = ResourceDef::prefix(r"/id/{id:\d{3}}");
-        assert!(resource.is_match("/id/1234"));
-        assert_eq!(resource.find_match("/id/1234"), Some(7));
-
         let resource = ResourceDef::prefix("/user");
         // input string shorter than prefix
         assert!(resource.find_match("/foo").is_none());
@@ -1551,6 +1575,21 @@ mod tests {
         assert!(path.get("uid").is_some());
     }
 
+    #[test]
+    fn dynamic_prefix_proper_segmentation() {
+        let resource = ResourceDef::prefix(r"/id/{id:\d{3}}");
+
+        assert!(resource.is_match("/id/123"));
+        assert!(resource.is_match("/id/123/foo"));
+        assert!(!resource.is_match("/id/1234"));
+        assert!(!resource.is_match("/id/123a"));
+
+        assert_eq!(resource.find_match("/id/123"), Some(7));
+        assert_eq!(resource.find_match("/id/123/foo"), Some(7));
+        assert_eq!(resource.find_match("/id/1234"), None);
+        assert_eq!(resource.find_match("/id/123a"), None);
+    }
+
     #[test]
     fn build_path_map() {
         let resource = ResourceDef::new("/user/{item1}/{item2}/");


Test patch: