1use aho_corasick::AhoCorasick;
2use matchit::Router as MatchitRouter;
3use regex::Regex;
4use reinhardt_http::PathParams;
5use std::collections::{HashMap, HashSet};
6
7const MAX_PATTERN_LENGTH: usize = 1024;
11
12const MAX_PATH_SEGMENTS: usize = 32;
16
17const MAX_REGEX_SIZE: usize = 1 << 20; fn type_spec_to_regex(type_spec: &str) -> &'static str {
42 match type_spec {
43 "int" => r"[0-9]+",
45 "str" => r"[^/]+",
46 "uuid" => r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
47 "slug" => r"[a-z0-9]+(?:-[a-z0-9]+)*",
48 "path" => r".+",
54 "i8" | "i16" | "i32" | "i64" => r"-?[0-9]+",
56 "u8" | "u16" | "u32" | "u64" => r"[0-9]+",
58 "f32" | "f64" => r"-?[0-9]+(?:\.[0-9]+)?",
60 "bool" => r"true|false|1|0",
62 "email" => r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
63 "date" => r"[0-9]{4}-[0-9]{2}-[0-9]{2}",
64 _ => r"[^/]+",
66 }
67}
68
69fn validate_path_param(value: &str) -> bool {
80 if value.contains('\0') {
82 return false;
83 }
84
85 let lower = value.to_ascii_lowercase();
88 if lower.contains("%2e")
89 || lower.contains("%2f")
90 || lower.contains("%5c")
91 || lower.contains("%00")
92 {
93 return false;
94 }
95
96 if value.starts_with('/') || value.starts_with('\\') {
98 return false;
99 }
100
101 for segment in value.split('/') {
103 if segment == ".." {
104 return false;
105 }
106 }
107 for segment in value.split('\\') {
109 if segment == ".." {
110 return false;
111 }
112 }
113
114 true
115}
116
117pub(crate) fn validate_reverse_param(value: &str) -> bool {
127 if value.contains('\0') {
129 return false;
130 }
131
132 if value.contains('/') || value.contains('\\') || value.contains('?') || value.contains('#') {
134 return false;
135 }
136
137 if value == ".." || value.starts_with("../") || value.ends_with("/..") || value.contains("/../")
139 {
140 return false;
141 }
142
143 let lower = value.to_ascii_lowercase();
145 if lower.contains("%2f")
146 || lower.contains("%2e")
147 || lower.contains("%5c")
148 || lower.contains("%3f")
149 || lower.contains("%23")
150 || lower.contains("%00")
151 {
152 return false;
153 }
154
155 true
156}
157
158#[derive(Clone, Debug)]
161pub struct PathPattern {
162 pattern: String,
164 normalized_pattern: String,
166 regex: Regex,
167 param_names: Vec<String>,
168 path_type_params: HashSet<String>,
171 aho_corasick: Option<AhoCorasick>,
174}
175
176struct ParsePatternResult {
178 regex_str: String,
179 param_names: Vec<String>,
180 path_type_params: HashSet<String>,
182 normalized_pattern: String,
185}
186
187impl PathPattern {
188 pub fn new(pattern: impl Into<String>) -> Result<Self, String> {
205 let pattern = pattern.into();
206
207 if pattern.len() > MAX_PATTERN_LENGTH {
209 return Err(format!(
210 "Pattern length {} exceeds maximum allowed length of {} bytes",
211 pattern.len(),
212 MAX_PATTERN_LENGTH
213 ));
214 }
215
216 let segment_count = pattern.split('/').count();
218 if segment_count > MAX_PATH_SEGMENTS {
219 return Err(format!(
220 "Pattern has {} path segments, exceeding maximum of {}",
221 segment_count, MAX_PATH_SEGMENTS
222 ));
223 }
224
225 let parse_result = Self::parse_pattern(&pattern)?;
226
227 let regex = regex::RegexBuilder::new(&parse_result.regex_str)
229 .size_limit(MAX_REGEX_SIZE)
230 .build()
231 .map_err(|e| format!("Failed to compile pattern regex: {}", e))?;
232
233 let aho_corasick = if !parse_result.param_names.is_empty() {
235 let placeholders: Vec<String> = parse_result
236 .param_names
237 .iter()
238 .map(|name| format!("{{{}}}", name))
239 .collect();
240
241 AhoCorasick::new(&placeholders)
242 .map(Some)
243 .map_err(|e| format!("Failed to build Aho-Corasick automaton: {}", e))?
244 } else {
245 None
246 };
247
248 Ok(Self {
249 pattern,
250 normalized_pattern: parse_result.normalized_pattern,
251 regex,
252 param_names: parse_result.param_names,
253 path_type_params: parse_result.path_type_params,
254 aho_corasick,
255 })
256 }
257
258 fn parse_pattern(pattern: &str) -> Result<ParsePatternResult, String> {
259 let mut regex_str = String::from("^");
260 let mut param_names = Vec::new();
261 let mut path_type_params = HashSet::new();
262 let mut normalized_pattern = String::new();
263 let mut chars = pattern.chars().peekable();
264
265 while let Some(ch) = chars.next() {
266 match ch {
267 '{' => {
268 let mut param_content = String::new();
270 while let Some(&next_ch) = chars.peek() {
271 if next_ch == '}' {
272 chars.next(); break;
274 }
275 param_content.push(chars.next().unwrap());
276 }
277
278 if param_content.is_empty() {
279 return Err("Empty parameter name".to_string());
280 }
281
282 let (param_name, regex_pattern) =
284 if param_content.starts_with('<') && param_content.ends_with('>') {
285 let inner = ¶m_content[1..param_content.len() - 1]; if let Some(colon_pos) = inner.find(':') {
288 let type_spec = &inner[..colon_pos];
289 let name = &inner[colon_pos + 1..];
290 if name.is_empty() {
291 return Err(format!(
292 "Empty parameter name in typed parameter: {{<{}:>}}",
293 type_spec
294 ));
295 }
296 if type_spec == "path" {
297 path_type_params.insert(name.to_string());
298 }
299 (name.to_string(), type_spec_to_regex(type_spec))
300 } else {
301 return Err(format!(
302 "Invalid typed parameter syntax: {{<{}>}}. Expected {{<type:name>}}",
303 inner
304 ));
305 }
306 } else {
307 (param_content, "[^/]+")
309 };
310
311 param_names.push(param_name.clone());
312 regex_str.push_str(&format!("(?P<{}>{})", param_name, regex_pattern));
313 normalized_pattern.push_str(&format!("{{{}}}", param_name));
315 }
316 _ => {
317 if ".*+?^${}()|[]\\".contains(ch) {
319 regex_str.push('\\');
320 }
321 regex_str.push(ch);
322 normalized_pattern.push(ch);
324 }
325 }
326 }
327
328 regex_str.push('$');
329 Ok(ParsePatternResult {
330 regex_str,
331 param_names,
332 path_type_params,
333 normalized_pattern,
334 })
335 }
336 pub fn pattern(&self) -> &str {
347 &self.pattern
348 }
349
350 pub(crate) fn to_matchit_pattern(&self) -> String {
356 let mut result = String::new();
357 let mut chars = self.pattern.chars().peekable();
358
359 while let Some(ch) = chars.next() {
360 if ch == '{' {
361 let mut param_content = String::new();
362 while let Some(&next_ch) = chars.peek() {
363 if next_ch == '}' {
364 chars.next();
365 break;
366 }
367 param_content.push(chars.next().unwrap());
368 }
369
370 if param_content.starts_with('<') && param_content.ends_with('>') {
372 let inner = ¶m_content[1..param_content.len() - 1];
373 if let Some(colon_pos) = inner.find(':') {
374 let type_spec = &inner[..colon_pos];
375 let name = &inner[colon_pos + 1..];
376 if type_spec == "path" {
377 result.push_str(&format!("{{*{}}}", name));
379 } else {
380 result.push_str(&format!("{{{}}}", name));
382 }
383 } else {
384 result.push_str(&format!("{{{}}}", param_content));
385 }
386 } else {
387 result.push_str(&format!("{{{}}}", param_content));
389 }
390 } else {
391 result.push(ch);
392 }
393 }
394
395 result
396 }
397 pub fn param_names(&self) -> &[String] {
408 &self.param_names
409 }
410
411 pub fn is_match(&self, path: &str) -> bool {
423 self.regex.is_match(path)
424 }
425
426 pub fn extract_params(&self, path: &str) -> Option<HashMap<String, String>> {
438 self.regex.captures(path).and_then(|captures| {
439 let mut params = HashMap::new();
440 for name in self.param_names() {
441 if let Some(value) = captures.name(name) {
442 let val = value.as_str();
443 if self.path_type_params.contains(name) && !validate_path_param(val) {
445 return None;
446 }
447 params.insert(name.clone(), val.to_string());
448 }
449 }
450 Some(params)
451 })
452 }
453
454 pub fn reverse(&self, params: &HashMap<String, String>) -> Result<String, String> {
487 for param_name in &self.param_names {
489 if !params.contains_key(param_name) {
490 return Err(format!("Missing required parameter: {}", param_name));
491 }
492 }
493
494 for (name, value) in params {
496 if !validate_reverse_param(value) {
497 return Err(format!(
498 "Invalid parameter value for '{}': contains dangerous characters",
499 name
500 ));
501 }
502 }
503
504 if self.param_names.is_empty() {
506 return Ok(self.normalized_pattern.clone());
507 }
508
509 match &self.aho_corasick {
511 Some(ac) => {
512 let mut replacements = Vec::new();
514 for mat in ac.find_iter(&self.normalized_pattern) {
515 let param_name = &self.param_names[mat.pattern()];
516 let value = params.get(param_name).unwrap();
518 replacements.push((mat.start(), mat.end(), value.clone()));
519 }
520
521 let mut result = self.normalized_pattern.clone();
523 for (start, end, value) in replacements.into_iter().rev() {
524 result.replace_range(start..end, &value);
525 }
526
527 Ok(result)
528 }
529 None => {
530 Ok(self.normalized_pattern.clone())
532 }
533 }
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum MatchingMode {
540 Linear,
542 RadixTree,
544}
545
546pub struct PathMatcher {
552 patterns: Vec<(PathPattern, String)>, radix_router: Option<RadixRouter>,
554 mode: MatchingMode,
555}
556
557impl PathMatcher {
558 pub fn new() -> Self {
569 Self {
570 patterns: Vec::new(),
571 radix_router: None,
572 mode: MatchingMode::Linear,
573 }
574 }
575
576 pub fn with_mode(mode: MatchingMode) -> Self {
586 Self {
587 patterns: Vec::new(),
588 radix_router: if mode == MatchingMode::RadixTree {
589 Some(RadixRouter::new())
590 } else {
591 None
592 },
593 mode,
594 }
595 }
596
597 pub fn enable_radix_tree(&mut self) {
618 if self.mode == MatchingMode::RadixTree {
619 return; }
621
622 self.mode = MatchingMode::RadixTree;
623 let mut radix_router = RadixRouter::new();
624
625 for (pattern, handler_id) in &self.patterns {
627 let _ = radix_router.add_route(&pattern.to_matchit_pattern(), handler_id.clone());
628 }
629
630 self.radix_router = Some(radix_router);
631 }
632
633 pub fn mode(&self) -> MatchingMode {
635 self.mode
636 }
637 pub fn add_pattern(&mut self, pattern: PathPattern, handler_id: String) {
654 let matchit_pattern = pattern.to_matchit_pattern();
655 self.patterns.push((pattern, handler_id.clone()));
656
657 if let Some(ref mut radix_router) = self.radix_router {
659 let _ = radix_router.add_route(&matchit_pattern, handler_id);
660 }
661 }
662 pub fn match_path(&self, path: &str) -> Option<(String, PathParams)> {
693 match self.mode {
694 MatchingMode::RadixTree => {
695 if let Some(ref radix_router) = self.radix_router {
697 let (handler_id, params) = radix_router.match_path(path)?;
698
699 if let Some((pattern, _)) =
701 self.patterns.iter().find(|(_, id)| *id == handler_id)
702 {
703 for (name, value) in params.iter() {
704 if pattern.path_type_params.contains(name)
705 && !validate_path_param(value)
706 {
707 return None;
708 }
709 }
710 }
711
712 Some((handler_id, params))
713 } else {
714 self.match_path_linear(path)
716 }
717 }
718 MatchingMode::Linear => {
719 self.match_path_linear(path)
721 }
722 }
723 }
724
725 fn match_path_linear(&self, path: &str) -> Option<(String, PathParams)> {
727 'outer: for (pattern, handler_id) in &self.patterns {
728 if let Some(captures) = pattern.regex.captures(path) {
729 let mut params = PathParams::new();
733
734 for name in pattern.param_names() {
735 if let Some(value) = captures.name(name) {
736 let val = value.as_str();
737 if pattern.path_type_params.contains(name) && !validate_path_param(val) {
739 continue 'outer;
740 }
741 params.insert(name.clone(), val.to_string());
742 }
743 }
744
745 return Some((handler_id.clone(), params));
746 }
747 }
748
749 None
750 }
751}
752
753impl Default for PathMatcher {
754 fn default() -> Self {
755 Self::new()
756 }
757}
758
759#[derive(Debug, thiserror::Error)]
761pub enum RadixRouterError {
762 #[error("Invalid pattern: {0}")]
764 InvalidPattern(String),
765 #[error("Route insertion failed: {0}")]
767 InsertionFailed(String),
768}
769
770pub struct RadixRouter {
807 router: MatchitRouter<String>,
808}
809
810impl RadixRouter {
811 pub fn new() -> Self {
821 Self {
822 router: MatchitRouter::new(),
823 }
824 }
825
826 pub fn add_route(&mut self, pattern: &str, handler_id: String) -> Result<(), RadixRouterError> {
854 self.router
855 .insert(pattern, handler_id)
856 .map_err(|e| RadixRouterError::InsertionFailed(e.to_string()))
857 }
858
859 pub fn match_path(&self, path: &str) -> Option<(String, PathParams)> {
888 match self.router.at(path) {
889 Ok(matched) => {
890 let handler_id = matched.value.clone();
891 let params: PathParams = matched
895 .params
896 .iter()
897 .map(|(k, v)| (k.to_string(), v.to_string()))
898 .collect();
899
900 Some((handler_id, params))
901 }
902 Err(_) => None,
903 }
904 }
905}
906
907impl Default for RadixRouter {
908 fn default() -> Self {
909 Self::new()
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 #[test]
918 fn test_simple_pattern() {
919 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/")).unwrap();
920 assert!(pattern.regex.is_match("/users/"));
921 assert!(!pattern.regex.is_match("/users/123/"));
922 }
923
924 #[test]
925 fn test_parameter_pattern() {
926 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
927 assert_eq!(pattern.param_names(), &["id"]);
928 assert!(pattern.regex.is_match("/users/123/"));
929 assert!(!pattern.regex.is_match("/users/"));
930 }
931
932 #[test]
933 fn test_pattern_multiple_parameters() {
934 let pattern = PathPattern::new(reinhardt_routers_macros::path!(
935 "/users/{user_id}/posts/{post_id}/"
936 ))
937 .unwrap();
938 assert_eq!(pattern.param_names(), &["user_id", "post_id"]);
939 assert!(pattern.regex.is_match("/users/123/posts/456/"));
940 }
941
942 #[test]
943 fn test_path_matcher() {
944 let mut matcher = PathMatcher::new();
945 matcher.add_pattern(
946 PathPattern::new(reinhardt_routers_macros::path!("/users/")).unwrap(),
947 "users_list".to_string(),
948 );
949 matcher.add_pattern(
950 PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap(),
951 "users_detail".to_string(),
952 );
953
954 let result = matcher.match_path("/users/123/");
955 assert!(result.is_some());
956 let (handler_id, params) = result.unwrap();
957 assert_eq!(handler_id, "users_detail");
958 assert_eq!(params.get("id"), Some(&"123".to_string()));
959 }
960
961 #[test]
966 fn test_reverse_simple_pattern_no_params() {
967 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/")).unwrap();
968 let params = HashMap::new();
969
970 let result = pattern.reverse(¶ms).unwrap();
971 assert_eq!(result, "/users/");
972 }
973
974 #[test]
975 fn test_reverse_single_parameter() {
976 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
977 let mut params = HashMap::new();
978 params.insert("id".to_string(), "123".to_string());
979
980 let result = pattern.reverse(¶ms).unwrap();
981 assert_eq!(result, "/users/123/");
982 }
983
984 #[test]
985 fn test_reverse_multiple_parameters() {
986 let pattern = PathPattern::new(reinhardt_routers_macros::path!(
987 "/users/{user_id}/posts/{post_id}/"
988 ))
989 .unwrap();
990 let mut params = HashMap::new();
991 params.insert("user_id".to_string(), "42".to_string());
992 params.insert("post_id".to_string(), "100".to_string());
993
994 let result = pattern.reverse(¶ms).unwrap();
995 assert_eq!(result, "/users/42/posts/100/");
996 }
997
998 #[test]
999 fn test_reverse_many_parameters() {
1000 let pattern = PathPattern::new(
1002 "/api/{p1}/{p2}/{p3}/{p4}/{p5}/{p6}/{p7}/{p8}/{p9}/{p10}/{p11}/{p12}/",
1003 )
1004 .unwrap();
1005
1006 let mut params = HashMap::new();
1007 params.insert("p1".to_string(), "v1".to_string());
1008 params.insert("p2".to_string(), "v2".to_string());
1009 params.insert("p3".to_string(), "v3".to_string());
1010 params.insert("p4".to_string(), "v4".to_string());
1011 params.insert("p5".to_string(), "v5".to_string());
1012 params.insert("p6".to_string(), "v6".to_string());
1013 params.insert("p7".to_string(), "v7".to_string());
1014 params.insert("p8".to_string(), "v8".to_string());
1015 params.insert("p9".to_string(), "v9".to_string());
1016 params.insert("p10".to_string(), "v10".to_string());
1017 params.insert("p11".to_string(), "v11".to_string());
1018 params.insert("p12".to_string(), "v12".to_string());
1019
1020 let result = pattern.reverse(¶ms).unwrap();
1021 assert_eq!(result, "/api/v1/v2/v3/v4/v5/v6/v7/v8/v9/v10/v11/v12/");
1022 }
1023
1024 #[test]
1025 fn test_reverse_consecutive_placeholders() {
1026 let pattern = PathPattern::new("/{a}{b}/").unwrap();
1027 let mut params = HashMap::new();
1028 params.insert("a".to_string(), "1".to_string());
1029 params.insert("b".to_string(), "2".to_string());
1030
1031 let result = pattern.reverse(¶ms).unwrap();
1032 assert_eq!(result, "/12/");
1033 }
1034
1035 #[test]
1036 fn test_reverse_missing_parameter() {
1037 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1038 let params = HashMap::new();
1039
1040 let result = pattern.reverse(¶ms);
1041 assert!(result.is_err());
1042 assert!(
1043 result
1044 .unwrap_err()
1045 .contains("Missing required parameter: id")
1046 );
1047 }
1048
1049 #[test]
1050 fn test_reverse_partial_parameters() {
1051 let pattern = PathPattern::new(reinhardt_routers_macros::path!(
1052 "/users/{user_id}/posts/{post_id}/"
1053 ))
1054 .unwrap();
1055 let mut params = HashMap::new();
1056 params.insert("user_id".to_string(), "42".to_string());
1057 let result = pattern.reverse(¶ms);
1060 assert!(result.is_err());
1061 assert!(result.unwrap_err().contains("Missing required parameter"));
1062 }
1063
1064 #[test]
1065 fn test_reverse_special_chars_in_values() {
1066 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/items/{id}/")).unwrap();
1067 let mut params = HashMap::new();
1068 params.insert("id".to_string(), "foo-bar_123".to_string());
1069
1070 let result = pattern.reverse(¶ms).unwrap();
1071 assert_eq!(result, "/items/foo-bar_123/");
1072 }
1073
1074 #[test]
1075 fn test_reverse_numeric_values() {
1076 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/items/{id}/")).unwrap();
1077 let mut params = HashMap::new();
1078 params.insert("id".to_string(), "12345".to_string());
1079
1080 let result = pattern.reverse(¶ms).unwrap();
1081 assert_eq!(result, "/items/12345/");
1082 }
1083
1084 #[test]
1085 fn test_reverse_unicode_values() {
1086 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{name}/")).unwrap();
1087 let mut params = HashMap::new();
1088 params.insert("name".to_string(), "ユーザー".to_string());
1089
1090 let result = pattern.reverse(¶ms).unwrap();
1091 assert_eq!(result, "/users/ユーザー/");
1092 }
1093
1094 #[test]
1095 fn test_reverse_param_at_start() {
1096 let pattern = PathPattern::new("{lang}/users/").unwrap();
1097 let mut params = HashMap::new();
1098 params.insert("lang".to_string(), "ja".to_string());
1099
1100 let result = pattern.reverse(¶ms).unwrap();
1101 assert_eq!(result, "ja/users/");
1102 }
1103
1104 #[test]
1105 fn test_reverse_param_at_end() {
1106 let pattern = PathPattern::new("/api/data.{format}").unwrap();
1107 let mut params = HashMap::new();
1108 params.insert("format".to_string(), "json".to_string());
1109
1110 let result = pattern.reverse(¶ms).unwrap();
1111 assert_eq!(result, "/api/data.json");
1112 }
1113
1114 #[test]
1115 fn test_reverse_complex_mixed_content() {
1116 let pattern = PathPattern::new("/items/{id}/actions/{action}/execute").unwrap();
1117 let mut params = HashMap::new();
1118 params.insert("id".to_string(), "123".to_string());
1119 params.insert("action".to_string(), "edit".to_string());
1120
1121 let result = pattern.reverse(¶ms).unwrap();
1122 assert_eq!(result, "/items/123/actions/edit/execute");
1123 }
1124
1125 #[test]
1126 fn test_reverse_long_value() {
1127 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/items/{id}/")).unwrap();
1128 let mut params = HashMap::new();
1129 let long_id = "a".repeat(1000);
1130 params.insert("id".to_string(), long_id.clone());
1131
1132 let result = pattern.reverse(¶ms).unwrap();
1133 assert_eq!(result, format!("/items/{}/", long_id));
1134 }
1135
1136 #[test]
1137 fn test_reverse_empty_value() {
1138 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/items/{id}/")).unwrap();
1139 let mut params = HashMap::new();
1140 params.insert("id".to_string(), "".to_string());
1141
1142 let result = pattern.reverse(¶ms).unwrap();
1143 assert_eq!(result, "/items//");
1144 }
1145
1146 #[test]
1147 fn test_reverse_extra_parameters() {
1148 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1150 let mut params = HashMap::new();
1151 params.insert("id".to_string(), "123".to_string());
1152 params.insert("extra".to_string(), "ignored".to_string());
1153
1154 let result = pattern.reverse(¶ms).unwrap();
1155 assert_eq!(result, "/users/123/");
1156 }
1157
1158 #[test]
1163 fn test_radix_router_basic_matching() {
1164 let mut router = RadixRouter::new();
1165 router
1166 .add_route("/users/", "users_list".to_string())
1167 .unwrap();
1168 router
1169 .add_route("/users/{id}/", "users_detail".to_string())
1170 .unwrap();
1171
1172 let result = router.match_path("/users/");
1174 assert!(result.is_some());
1175 let (handler_id, params) = result.unwrap();
1176 assert_eq!(handler_id, "users_list");
1177 assert!(params.is_empty());
1178
1179 let result = router.match_path("/users/123/");
1181 assert!(result.is_some());
1182 let (handler_id, params) = result.unwrap();
1183 assert_eq!(handler_id, "users_detail");
1184 assert_eq!(params.get("id"), Some(&"123".to_string()));
1185 }
1186
1187 #[test]
1188 fn test_radix_router_multiple_parameters() {
1189 let mut router = RadixRouter::new();
1190 router
1191 .add_route("/users/{id}/posts/{post_id}/", "post_detail".to_string())
1192 .unwrap();
1193
1194 let result = router.match_path("/users/123/posts/456/");
1195 assert!(result.is_some());
1196 let (handler_id, params) = result.unwrap();
1197 assert_eq!(handler_id, "post_detail");
1198 assert_eq!(params.get("id"), Some(&"123".to_string()));
1199 assert_eq!(params.get("post_id"), Some(&"456".to_string()));
1200 }
1201
1202 #[test]
1203 fn test_radix_router_wildcard() {
1204 let mut router = RadixRouter::new();
1205 router
1206 .add_route("/files/{*path}", "serve_file".to_string())
1207 .unwrap();
1208
1209 let result = router.match_path("/files/images/logo.png");
1210 assert!(result.is_some());
1211 let (handler_id, params) = result.unwrap();
1212 assert_eq!(handler_id, "serve_file");
1213 assert_eq!(params.get("path"), Some(&"images/logo.png".to_string()));
1214 }
1215
1216 #[test]
1217 fn test_radix_router_no_match() {
1218 let mut router = RadixRouter::new();
1219 router
1220 .add_route("/users/", "users_list".to_string())
1221 .unwrap();
1222
1223 let result = router.match_path("/posts/");
1224 assert!(result.is_none());
1225 }
1226
1227 #[test]
1228 fn test_path_matcher_radix_tree_mode() {
1229 let mut matcher = PathMatcher::with_mode(MatchingMode::RadixTree);
1230 matcher.add_pattern(
1231 PathPattern::new(reinhardt_routers_macros::path!("/users/")).unwrap(),
1232 "users_list".to_string(),
1233 );
1234 matcher.add_pattern(
1235 PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap(),
1236 "users_detail".to_string(),
1237 );
1238
1239 assert_eq!(matcher.mode(), MatchingMode::RadixTree);
1240
1241 let result = matcher.match_path("/users/123/");
1242 assert!(result.is_some());
1243 let (handler_id, params) = result.unwrap();
1244 assert_eq!(handler_id, "users_detail");
1245 assert_eq!(params.get("id"), Some(&"123".to_string()));
1246 }
1247
1248 #[test]
1249 fn test_path_matcher_enable_radix_tree() {
1250 let mut matcher = PathMatcher::new();
1251 matcher.add_pattern(
1252 PathPattern::new(reinhardt_routers_macros::path!("/users/")).unwrap(),
1253 "users_list".to_string(),
1254 );
1255
1256 assert_eq!(matcher.mode(), MatchingMode::Linear);
1258
1259 matcher.enable_radix_tree();
1261 assert_eq!(matcher.mode(), MatchingMode::RadixTree);
1262
1263 let result = matcher.match_path("/users/");
1265 assert!(result.is_some());
1266 }
1267
1268 #[test]
1269 fn test_path_matcher_linear_vs_radix() {
1270 let mut linear_matcher = PathMatcher::new();
1272 let mut radix_matcher = PathMatcher::with_mode(MatchingMode::RadixTree);
1273
1274 for i in 1..=10 {
1275 let pattern = PathPattern::new(format!("/route{}/{{id}}/", i)).unwrap();
1276 linear_matcher.add_pattern(pattern.clone(), format!("handler_{}", i));
1277 radix_matcher.add_pattern(pattern, format!("handler_{}", i));
1278 }
1279
1280 for i in 1..=10 {
1282 let path = format!("/route{}/123/", i);
1283 let linear_result = linear_matcher.match_path(&path);
1284 let radix_result = radix_matcher.match_path(&path);
1285
1286 assert_eq!(linear_result, radix_result);
1287 assert!(linear_result.is_some());
1288 }
1289 }
1290
1291 #[test]
1296 fn test_path_type_rejects_traversal() {
1297 let pattern = PathPattern::new("/files/{<path:filepath>}").unwrap();
1299
1300 assert!(
1302 pattern
1303 .extract_params("/files/../../../etc/passwd")
1304 .is_none(),
1305 "Path type should reject directory traversal"
1306 );
1307 assert!(
1308 pattern
1309 .extract_params("/files/foo/../../etc/passwd")
1310 .is_none(),
1311 "Path type should reject embedded directory traversal"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_path_type_allows_valid_paths() {
1317 let pattern = PathPattern::new("/files/{<path:filepath>}").unwrap();
1319
1320 let result = pattern.extract_params("/files/images/logo.png");
1322
1323 assert!(result.is_some());
1325 let params = result.unwrap();
1326 assert_eq!(params.get("filepath"), Some(&"images/logo.png".to_string()));
1327 }
1328
1329 #[test]
1330 fn test_path_type_allows_dotfiles() {
1331 let pattern = PathPattern::new("/files/{<path:filepath>}").unwrap();
1333
1334 let result = pattern.extract_params("/files/.gitignore");
1336
1337 assert!(result.is_some());
1339 let params = result.unwrap();
1340 assert_eq!(params.get("filepath"), Some(&".gitignore".to_string()));
1341 }
1342
1343 #[test]
1344 fn test_path_type_matcher_rejects_traversal() {
1345 let mut matcher = PathMatcher::new();
1347 matcher.add_pattern(
1348 PathPattern::new("/files/{<path:filepath>}").unwrap(),
1349 "serve_file".to_string(),
1350 );
1351
1352 assert!(
1354 matcher.match_path("/files/../../../etc/passwd").is_none(),
1355 "PathMatcher should reject directory traversal in path params"
1356 );
1357
1358 let result = matcher.match_path("/files/css/style.css");
1360 assert!(result.is_some());
1361 }
1362
1363 #[test]
1364 fn test_validate_path_param_function() {
1365 assert!(validate_path_param("images/logo.png"));
1367 assert!(validate_path_param("css/style.css"));
1368 assert!(validate_path_param(".gitignore"));
1369 assert!(validate_path_param("dir/.hidden"));
1370
1371 assert!(!validate_path_param("../etc/passwd"));
1373 assert!(!validate_path_param("foo/../../bar"));
1374 assert!(!validate_path_param(".."));
1375 assert!(!validate_path_param("foo/.."));
1376
1377 assert!(!validate_path_param("foo\0bar"));
1379 }
1380
1381 #[test]
1386 fn test_validate_path_param_rejects_encoded_traversal() {
1387 assert!(!validate_path_param("%2e%2e/%2e%2e/etc/passwd"));
1390 assert!(!validate_path_param("foo/%2e%2e/bar"));
1391 assert!(!validate_path_param("%2E%2E/secret"));
1392
1393 assert!(!validate_path_param("foo%2fbar"));
1395 assert!(!validate_path_param("..%2f..%2fetc%2fpasswd"));
1396 assert!(!validate_path_param("foo%2Fbar"));
1397
1398 assert!(!validate_path_param("foo%5cbar"));
1400 assert!(!validate_path_param("..%5C..%5Csecret"));
1401
1402 assert!(!validate_path_param("file%00.txt"));
1404 }
1405
1406 #[test]
1407 fn test_validate_path_param_rejects_absolute_paths() {
1408 assert!(!validate_path_param("/etc/passwd"));
1410 assert!(!validate_path_param("\\windows\\system32"));
1411 }
1412
1413 #[test]
1414 fn test_path_type_rejects_encoded_traversal() {
1415 let pattern = PathPattern::new("/files/{<path:filepath>}").unwrap();
1417
1418 assert!(
1420 pattern
1421 .extract_params("/files/%2e%2e/%2e%2e/etc/passwd")
1422 .is_none(),
1423 "Path type should reject percent-encoded traversal"
1424 );
1425 assert!(
1426 pattern
1427 .extract_params("/files/..%2f..%2fetc%2fpasswd")
1428 .is_none(),
1429 "Path type should reject mixed encoded traversal"
1430 );
1431 assert!(
1432 pattern.extract_params("/files/foo%00bar").is_none(),
1433 "Path type should reject encoded null bytes"
1434 );
1435 }
1436
1437 #[test]
1438 fn test_path_type_rejects_absolute_path_param() {
1439 let pattern = PathPattern::new("/files/{<path:filepath>}").unwrap();
1441
1442 assert!(
1445 pattern.extract_params("/files//etc/passwd").is_none(),
1446 "Path type should reject absolute path in parameter"
1447 );
1448 }
1449
1450 #[test]
1451 fn test_radix_tree_mode_rejects_traversal() {
1452 let mut matcher = PathMatcher::with_mode(MatchingMode::RadixTree);
1454 matcher.add_pattern(
1455 PathPattern::new("/files/{<path:filepath>}").unwrap(),
1456 "serve_file".to_string(),
1457 );
1458
1459 assert!(
1461 matcher.match_path("/files/../../../etc/passwd").is_none(),
1462 "RadixTree mode should reject directory traversal in path params"
1463 );
1464 assert!(
1465 matcher.match_path("/files/foo/../../etc/passwd").is_none(),
1466 "RadixTree mode should reject embedded directory traversal"
1467 );
1468
1469 let result = matcher.match_path("/files/css/style.css");
1471 assert!(result.is_some());
1472 let (handler_id, params) = result.unwrap();
1473 assert_eq!(handler_id, "serve_file");
1474 assert_eq!(params.get("filepath"), Some(&"css/style.css".to_string()));
1475 }
1476
1477 #[test]
1478 fn test_radix_tree_mode_rejects_encoded_traversal() {
1479 let mut matcher = PathMatcher::with_mode(MatchingMode::RadixTree);
1481 matcher.add_pattern(
1482 PathPattern::new("/files/{<path:filepath>}").unwrap(),
1483 "serve_file".to_string(),
1484 );
1485
1486 assert!(
1488 matcher
1489 .match_path("/files/%2e%2e/%2e%2e/etc/passwd")
1490 .is_none(),
1491 "RadixTree mode should reject percent-encoded traversal"
1492 );
1493 assert!(
1494 matcher
1495 .match_path("/files/..%2f..%2fetc%2fpasswd")
1496 .is_none(),
1497 "RadixTree mode should reject mixed encoded traversal"
1498 );
1499
1500 assert!(
1502 matcher.match_path("/files/foo%00bar").is_none(),
1503 "RadixTree mode should reject encoded null bytes"
1504 );
1505 }
1506
1507 #[test]
1512 fn test_reverse_rejects_path_separator_injection() {
1513 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1515 let mut params = HashMap::new();
1516 params.insert("id".to_string(), "123/../../admin".to_string());
1517
1518 let result = pattern.reverse(¶ms);
1520
1521 assert!(
1523 result.is_err(),
1524 "Reverse should reject path separators in parameter values"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_reverse_rejects_query_string_injection() {
1530 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1532 let mut params = HashMap::new();
1533 params.insert("id".to_string(), "123?admin=true".to_string());
1534
1535 let result = pattern.reverse(¶ms);
1537
1538 assert!(
1540 result.is_err(),
1541 "Reverse should reject query string delimiters in parameter values"
1542 );
1543 }
1544
1545 #[test]
1546 fn test_reverse_rejects_fragment_injection() {
1547 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1549 let mut params = HashMap::new();
1550 params.insert("id".to_string(), "123#fragment".to_string());
1551
1552 let result = pattern.reverse(¶ms);
1554
1555 assert!(
1557 result.is_err(),
1558 "Reverse should reject fragment identifiers in parameter values"
1559 );
1560 }
1561
1562 #[test]
1563 fn test_reverse_rejects_encoded_injection() {
1564 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/")).unwrap();
1566 let mut params = HashMap::new();
1567 params.insert("id".to_string(), "123%2f..%2f..%2fadmin".to_string());
1568
1569 let result = pattern.reverse(¶ms);
1571
1572 assert!(
1574 result.is_err(),
1575 "Reverse should reject percent-encoded dangerous characters"
1576 );
1577 }
1578
1579 #[test]
1580 fn test_reverse_allows_safe_values() {
1581 let pattern =
1583 PathPattern::new(reinhardt_routers_macros::path!("/users/{id}/posts/{slug}/")).unwrap();
1584 let mut params = HashMap::new();
1585 params.insert("id".to_string(), "123".to_string());
1586 params.insert("slug".to_string(), "my-blog-post".to_string());
1587
1588 let result = pattern.reverse(¶ms);
1590
1591 assert!(result.is_ok());
1593 assert_eq!(result.unwrap(), "/users/123/posts/my-blog-post/");
1594 }
1595
1596 #[test]
1597 fn test_reverse_allows_unicode_values() {
1598 let pattern = PathPattern::new(reinhardt_routers_macros::path!("/users/{name}/")).unwrap();
1600 let mut params = HashMap::new();
1601 params.insert("name".to_string(), "ユーザー".to_string());
1602
1603 let result = pattern.reverse(¶ms);
1605
1606 assert!(result.is_ok());
1608 assert_eq!(result.unwrap(), "/users/ユーザー/");
1609 }
1610
1611 #[test]
1612 fn test_validate_reverse_param_function() {
1613 assert!(validate_reverse_param("123"));
1617 assert!(validate_reverse_param("my-slug"));
1618 assert!(validate_reverse_param("foo_bar"));
1619 assert!(validate_reverse_param("ユーザー"));
1620 assert!(validate_reverse_param("hello-world-123"));
1621
1622 assert!(!validate_reverse_param("foo/bar"));
1624 assert!(!validate_reverse_param("foo\\bar"));
1625
1626 assert!(!validate_reverse_param("foo?bar=1"));
1628 assert!(!validate_reverse_param("foo#bar"));
1629
1630 assert!(!validate_reverse_param("foo\0bar"));
1632
1633 assert!(!validate_reverse_param("foo%2fbar"));
1635 assert!(!validate_reverse_param("foo%2ebar"));
1636 assert!(!validate_reverse_param("foo%5cbar"));
1637 assert!(!validate_reverse_param("foo%3fbar"));
1638 assert!(!validate_reverse_param("foo%23bar"));
1639 assert!(!validate_reverse_param("foo%00bar"));
1640 }
1641
1642 #[test]
1647 fn test_pattern_rejects_excessive_length() {
1648 let long_pattern = "/".to_string() + &"a".repeat(1025);
1650
1651 let result = PathPattern::new(long_pattern);
1653
1654 assert!(result.is_err());
1656 assert!(
1657 result
1658 .unwrap_err()
1659 .contains("exceeds maximum allowed length")
1660 );
1661 }
1662
1663 #[test]
1664 fn test_pattern_accepts_within_length_limit() {
1665 let pattern = "/users/{id}/posts/{post_id}/";
1667
1668 let result = PathPattern::new(pattern);
1670
1671 assert!(result.is_ok());
1673 }
1674
1675 #[test]
1676 fn test_pattern_rejects_at_boundary() {
1677 let pattern = "/".to_string() + &"a/".repeat(512) + "end";
1679 if pattern.len() > MAX_PATTERN_LENGTH {
1680 let result = PathPattern::new(pattern);
1682
1683 assert!(result.is_err());
1685 }
1686 }
1687
1688 #[test]
1693 fn test_pattern_rejects_excessive_segments() {
1694 let segments: Vec<&str> = (0..35).map(|_| "seg").collect();
1696 let pattern = format!("/{}/", segments.join("/"));
1697
1698 let result = PathPattern::new(pattern);
1700
1701 assert!(result.is_err());
1703 assert!(result.unwrap_err().contains("exceeding maximum"));
1704 }
1705
1706 #[test]
1707 fn test_pattern_accepts_within_segment_limit() {
1708 let pattern = "/a/b/c/d/e/";
1710
1711 let result = PathPattern::new(pattern);
1713
1714 assert!(result.is_ok());
1716 }
1717
1718 #[test]
1719 fn test_pattern_accepts_at_segment_boundary() {
1720 let segments: Vec<String> = (0..MAX_PATH_SEGMENTS - 2)
1722 .map(|i| format!("s{}", i))
1723 .collect();
1724 let pattern = format!("/{}/", segments.join("/"));
1725
1726 let result = PathPattern::new(&pattern);
1728
1729 assert!(result.is_ok());
1731 }
1732}