1use once_cell::sync::Lazy;
5use regex::Regex;
6use regress;
7use rustc_hash::FxHashMap;
8
9const INTERCEPTION_ROUTE_MARKERS: [&str; 4] = ["(..)(..)", "(.)", "(..)", "(...)"];
10const NEXT_QUERY_PARAM_PREFIX: &str = "nxtP";
11const NEXT_INTERCEPTION_MARKER_PREFIX: &str = "nxtI";
12
13#[derive(Debug, Clone)]
14pub struct Group {
15 pub pos: usize,
16 pub repeat: bool,
17 pub optional: bool,
18}
19
20#[derive(Debug)]
21pub struct RouteRegex {
22 pub groups: FxHashMap<String, Group>,
23 pub regex: String,
24}
25
26#[derive(Debug)]
27pub struct NamedRouteRegex {
28 pub regex: RouteRegex,
29 pub named_regex: String,
30 pub route_keys: FxHashMap<String, String>,
31}
32
33#[derive(Debug)]
34pub struct NamedMiddlewareRegex {
35 pub named_regex: String,
36}
37
38struct ParsedParameter {
39 key: String,
40 repeat: bool,
41 optional: bool,
42}
43
44fn parse_parameter(param: &str) -> ParsedParameter {
51 let mut key = param.to_string();
52 let optional = key.starts_with('[') && key.ends_with(']');
53 if optional {
54 key = key[1..key.len() - 1].to_string();
55 }
56 let repeat = key.starts_with("...");
57 if repeat {
58 key = key[3..].to_string();
59 }
60 ParsedParameter {
61 key,
62 repeat,
63 optional,
64 }
65}
66
67fn escape_string_regexp(segment: &str) -> String {
68 regress::escape(segment)
69}
70
71fn remove_trailing_slash(route: &str) -> &str {
77 if route == "/" {
78 route
79 } else {
80 route.trim_end_matches('/')
81 }
82}
83
84static PARAM_MATCH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
85
86fn get_parametrized_route(route: &str) -> (String, FxHashMap<String, Group>) {
87 let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
88 let mut groups: FxHashMap<String, Group> = FxHashMap::default();
89 let mut group_index = 1;
90 let parameterized_route = segments
91 .iter()
92 .map(|segment| {
93 let marker_match = INTERCEPTION_ROUTE_MARKERS
94 .iter()
95 .find(|&&m| segment.starts_with(m))
96 .copied();
97 let param_matches = PARAM_MATCH_REGEX.captures(segment);
98 if let Some(matches) = param_matches {
99 let ParsedParameter {
100 key,
101 optional,
102 repeat,
103 } = parse_parameter(&matches[1]);
104 groups.insert(
105 key,
106 Group {
107 pos: group_index,
108 repeat,
109 optional,
110 },
111 );
112 group_index += 1;
113 if let Some(marker) = marker_match {
114 return format!("/{}([^/]+?)", escape_string_regexp(marker));
115 } else {
116 return match (repeat, optional) {
117 (true, true) => "(?:/(.+?))?",
118 (true, false) => "/(.+?)",
119 (false, true) => "(?:/([^/]+?))?",
120 (false, false) => "/([^/]+?)",
121 }
122 .to_string();
123 }
124 }
125 format!("/{}", escape_string_regexp(segment))
126 })
127 .collect::<Vec<String>>()
128 .join("");
129 (parameterized_route, groups)
130}
131
132pub fn get_route_regex(normalized_route: &str) -> RouteRegex {
136 let (parameterized_route, groups) = get_parametrized_route(normalized_route);
137 RouteRegex {
138 regex: format!("^{parameterized_route}(?:/)?$"),
139 groups,
140 }
141}
142
143fn build_get_safe_route_key() -> impl FnMut() -> String {
146 let mut i = 0;
147
148 move || {
149 let mut route_key = String::new();
150 i += 1;
151 let mut j = i;
152
153 while j > 0 {
154 route_key.push((97 + ((j - 1) % 26)) as u8 as char);
155 j = (j - 1) / 26;
156 }
157
158 i += 1;
159 route_key
160 }
161}
162
163fn get_safe_key_from_segment(
164 get_safe_route_key: &mut impl FnMut() -> String,
165 segment: &str,
166 route_keys: &mut FxHashMap<String, String>,
167 key_prefix: Option<&'static str>,
168 intercept_prefix: Option<&str>,
169) -> String {
170 let ParsedParameter {
171 key,
172 optional,
173 repeat,
174 } = parse_parameter(segment);
175
176 let mut cleaned_key = key.replace(|c: char| !c.is_alphanumeric(), "");
179 if let Some(prefix) = key_prefix {
180 cleaned_key = format!("{prefix}{cleaned_key}");
181 }
182 let mut invalid_key = false;
183
184 if cleaned_key.is_empty() || cleaned_key.len() > 30 {
187 invalid_key = true;
188 }
189 if cleaned_key.chars().next().unwrap().is_numeric() {
190 invalid_key = true;
191 }
192 if invalid_key {
193 cleaned_key = get_safe_route_key();
194 }
195 if let Some(prefix) = key_prefix {
196 route_keys.insert(cleaned_key.clone(), format!("{prefix}{key}"));
197 } else {
198 route_keys.insert(cleaned_key.clone(), key);
199 }
200
201 let intercept_prefix = intercept_prefix.map_or_else(String::new, escape_string_regexp);
202 match (repeat, optional) {
203 (true, true) => format!(r"(?:/{intercept_prefix}(?P<{cleaned_key}>.+?))?"),
204 (true, false) => format!(r"/{intercept_prefix}(?P<{cleaned_key}>.+?)"),
205 (false, true) => format!(r"(?:/{intercept_prefix}(?P<{cleaned_key}>[^/]+?))?"),
206 (false, false) => format!(r"/{intercept_prefix}(?P<{cleaned_key}>[^/]+?)"),
207 }
208}
209
210fn get_named_parametrized_route(
211 route: &str,
212 prefix_route_keys: bool,
213) -> (String, FxHashMap<String, String>) {
214 let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
215 let get_safe_route_key = &mut build_get_safe_route_key();
216 let mut route_keys: FxHashMap<String, String> = FxHashMap::default();
217 let parameterized_route = segments
218 .iter()
219 .map(|segment| {
220 let interception_marker = INTERCEPTION_ROUTE_MARKERS
221 .iter()
222 .find(|&m| segment.starts_with(m))
223 .copied();
224 let key_prefix = if prefix_route_keys {
225 if interception_marker.is_some() {
226 Some(NEXT_INTERCEPTION_MARKER_PREFIX)
227 } else {
228 Some(NEXT_QUERY_PARAM_PREFIX)
229 }
230 } else {
231 None
232 };
233 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
234 let param_matches = RE.captures(segment);
235 if let Some(matches) = param_matches {
236 return get_safe_key_from_segment(
237 get_safe_route_key,
238 &matches[1],
239 &mut route_keys,
240 key_prefix,
241 interception_marker,
242 );
243 }
244 format!("/{}", escape_string_regexp(segment))
245 })
246 .collect::<Vec<String>>()
247 .join("");
248 (parameterized_route, route_keys)
249}
250
251pub fn get_named_route_regex(normalized_route: &str) -> NamedRouteRegex {
259 let (parameterized_route, route_keys) = get_named_parametrized_route(normalized_route, false);
260 let regex = get_route_regex(normalized_route);
261 NamedRouteRegex {
262 regex,
263 named_regex: format!("^{parameterized_route}(?:/)?$"),
264 route_keys,
265 }
266}
267
268pub fn get_named_middleware_regex(normalized_route: &str) -> String {
271 let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true);
272 format!("^{parameterized_route}(?:/)?$")
273}
274
275#[cfg(test)]
276mod test {
277 use super::get_named_middleware_regex;
278
279 #[test]
280 fn should_properly_handle_intercept_routes() {
281 let tests = [
282 (
283 "/[locale]/example/(...)[locale]/intercepted",
284 "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\.\\)(?P<nxtIlocale>[^/]+?)/\
285 intercepted(?:/)?$",
286 ),
287 (
288 "/[locale]/example/(..)[locale]/intercepted",
289 "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\)(?P<nxtIlocale>[^/]+?)/intercepted(?\
290 :/)?$",
291 ),
292 (
293 "/[locale]/example/(.)[locale]/intercepted",
294 "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\)(?P<nxtIlocale>[^/]+?)/intercepted(?:/\
295 )?$",
296 ),
297 (
298 "/[locale]/example/(..)(..)[locale]/intercepted",
299 "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\)\\(\\.\\.\\)(?P<nxtIlocale>[^/]+?)/\
300 intercepted(?:/)?$",
301 ),
302 ];
303
304 for test in tests {
305 let intercept_route = get_named_middleware_regex(test.0);
306 assert_eq!(intercept_route, test.1);
307 }
308 }
309}