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