reinhardt_urls/routers/pattern/
path_pattern.rs1use super::validation::{
2 MAX_PATH_SEGMENTS, MAX_PATTERN_LENGTH, MAX_REGEX_SIZE, type_spec_to_regex, validate_path_param,
3 validate_reverse_param,
4};
5use aho_corasick::AhoCorasick;
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8
9#[derive(Clone, Debug)]
12pub struct PathPattern {
13 pattern: String,
15 normalized_pattern: String,
17 pub(super) regex: Regex,
18 pub(super) param_names: Vec<String>,
19 pub(super) path_type_params: HashSet<String>,
22 aho_corasick: Option<AhoCorasick>,
25}
26
27struct ParsePatternResult {
29 regex_str: String,
30 param_names: Vec<String>,
31 path_type_params: HashSet<String>,
33 normalized_pattern: String,
36}
37
38impl PathPattern {
39 pub fn new(pattern: impl Into<String>) -> Result<Self, String> {
56 let pattern = pattern.into();
57
58 if pattern.len() > MAX_PATTERN_LENGTH {
60 return Err(format!(
61 "Pattern length {} exceeds maximum allowed length of {} bytes",
62 pattern.len(),
63 MAX_PATTERN_LENGTH
64 ));
65 }
66
67 let segment_count = pattern.split('/').count();
69 if segment_count > MAX_PATH_SEGMENTS {
70 return Err(format!(
71 "Pattern has {} path segments, exceeding maximum of {}",
72 segment_count, MAX_PATH_SEGMENTS
73 ));
74 }
75
76 let parse_result = Self::parse_pattern(&pattern)?;
77
78 let regex = regex::RegexBuilder::new(&parse_result.regex_str)
80 .size_limit(MAX_REGEX_SIZE)
81 .build()
82 .map_err(|e| format!("Failed to compile pattern regex: {}", e))?;
83
84 let aho_corasick = if !parse_result.param_names.is_empty() {
86 let placeholders: Vec<String> = parse_result
87 .param_names
88 .iter()
89 .map(|name| format!("{{{}}}", name))
90 .collect();
91
92 AhoCorasick::new(&placeholders)
93 .map(Some)
94 .map_err(|e| format!("Failed to build Aho-Corasick automaton: {}", e))?
95 } else {
96 None
97 };
98
99 Ok(Self {
100 pattern,
101 normalized_pattern: parse_result.normalized_pattern,
102 regex,
103 param_names: parse_result.param_names,
104 path_type_params: parse_result.path_type_params,
105 aho_corasick,
106 })
107 }
108
109 fn parse_pattern(pattern: &str) -> Result<ParsePatternResult, String> {
110 let mut regex_str = String::from("^");
111 let mut param_names = Vec::new();
112 let mut path_type_params = HashSet::new();
113 let mut normalized_pattern = String::new();
114 let mut chars = pattern.chars().peekable();
115
116 while let Some(ch) = chars.next() {
117 match ch {
118 '{' => {
119 let mut param_content = String::new();
121 while let Some(&next_ch) = chars.peek() {
122 if next_ch == '}' {
123 chars.next(); break;
125 }
126 param_content.push(chars.next().unwrap());
127 }
128
129 if param_content.is_empty() {
130 return Err("Empty parameter name".to_string());
131 }
132
133 let (param_name, regex_pattern) =
135 if param_content.starts_with('<') && param_content.ends_with('>') {
136 let inner = ¶m_content[1..param_content.len() - 1]; if let Some(colon_pos) = inner.find(':') {
139 let type_spec = &inner[..colon_pos];
140 let name = &inner[colon_pos + 1..];
141 if name.is_empty() {
142 return Err(format!(
143 "Empty parameter name in typed parameter: {{<{}:>}}",
144 type_spec
145 ));
146 }
147 if type_spec == "path" {
148 path_type_params.insert(name.to_string());
149 }
150 (name.to_string(), type_spec_to_regex(type_spec))
151 } else {
152 return Err(format!(
153 "Invalid typed parameter syntax: {{<{}>}}. Expected {{<type:name>}}",
154 inner
155 ));
156 }
157 } else {
158 (param_content, "[^/]+")
160 };
161
162 param_names.push(param_name.clone());
163 regex_str.push_str(&format!("(?P<{}>{})", param_name, regex_pattern));
164 normalized_pattern.push_str(&format!("{{{}}}", param_name));
166 }
167 _ => {
168 if ".*+?^${}()|[]\\".contains(ch) {
170 regex_str.push('\\');
171 }
172 regex_str.push(ch);
173 normalized_pattern.push(ch);
175 }
176 }
177 }
178
179 regex_str.push('$');
180 Ok(ParsePatternResult {
181 regex_str,
182 param_names,
183 path_type_params,
184 normalized_pattern,
185 })
186 }
187 pub fn pattern(&self) -> &str {
198 &self.pattern
199 }
200
201 pub(crate) fn to_matchit_pattern(&self) -> String {
207 let mut result = String::new();
208 let mut chars = self.pattern.chars().peekable();
209
210 while let Some(ch) = chars.next() {
211 if ch == '{' {
212 let mut param_content = String::new();
213 while let Some(&next_ch) = chars.peek() {
214 if next_ch == '}' {
215 chars.next();
216 break;
217 }
218 param_content.push(chars.next().unwrap());
219 }
220
221 if param_content.starts_with('<') && param_content.ends_with('>') {
223 let inner = ¶m_content[1..param_content.len() - 1];
224 if let Some(colon_pos) = inner.find(':') {
225 let type_spec = &inner[..colon_pos];
226 let name = &inner[colon_pos + 1..];
227 if type_spec == "path" {
228 result.push_str(&format!("{{*{}}}", name));
230 } else {
231 result.push_str(&format!("{{{}}}", name));
233 }
234 } else {
235 result.push_str(&format!("{{{}}}", param_content));
236 }
237 } else {
238 result.push_str(&format!("{{{}}}", param_content));
240 }
241 } else {
242 result.push(ch);
243 }
244 }
245
246 result
247 }
248 pub fn param_names(&self) -> &[String] {
259 &self.param_names
260 }
261
262 pub fn is_match(&self, path: &str) -> bool {
274 self.regex.is_match(path)
275 }
276
277 pub fn extract_params(&self, path: &str) -> Option<HashMap<String, String>> {
289 self.regex.captures(path).and_then(|captures| {
290 let mut params = HashMap::new();
291 for name in self.param_names() {
292 if let Some(value) = captures.name(name) {
293 let val = value.as_str();
294 if self.path_type_params.contains(name) && !validate_path_param(val) {
296 return None;
297 }
298 params.insert(name.clone(), val.to_string());
299 }
300 }
301 Some(params)
302 })
303 }
304
305 pub fn reverse(&self, params: &HashMap<String, String>) -> Result<String, String> {
338 for param_name in &self.param_names {
340 if !params.contains_key(param_name) {
341 return Err(format!("Missing required parameter: {}", param_name));
342 }
343 }
344
345 for (name, value) in params {
347 if !validate_reverse_param(value) {
348 return Err(format!(
349 "Invalid parameter value for '{}': contains dangerous characters",
350 name
351 ));
352 }
353 }
354
355 if self.param_names.is_empty() {
357 return Ok(self.normalized_pattern.clone());
358 }
359
360 match &self.aho_corasick {
362 Some(ac) => {
363 let mut replacements = Vec::new();
365 for mat in ac.find_iter(&self.normalized_pattern) {
366 let param_name = &self.param_names[mat.pattern()];
367 let value = params.get(param_name).unwrap();
369 replacements.push((mat.start(), mat.end(), value.clone()));
370 }
371
372 let mut result = self.normalized_pattern.clone();
374 for (start, end, value) in replacements.into_iter().rev() {
375 result.replace_range(start..end, &value);
376 }
377
378 Ok(result)
379 }
380 None => {
381 Ok(self.normalized_pattern.clone())
383 }
384 }
385 }
386}