next_core/next_edge/
route_regex.rs1use once_cell::sync::Lazy;
5use regex::Regex;
6use rustc_hash::FxHashMap;
7
8const INTERCEPTION_ROUTE_MARKERS: [&str; 4] = ["(..)(..)", "(.)", "(..)", "(...)"];
9const NEXT_QUERY_PARAM_PREFIX: &str = "nxtP";
10const NEXT_INTERCEPTION_MARKER_PREFIX: &str = "nxtI";
11
12#[derive(Debug, Clone)]
13pub struct Group {
14 pub pos: usize,
15 pub repeat: bool,
16 pub optional: bool,
17}
18
19#[derive(Debug)]
20pub struct RouteRegex {
21 pub groups: FxHashMap<String, Group>,
22 pub regex: String,
23}
24
25#[derive(Debug)]
26pub struct NamedRouteRegex {
27 pub regex: RouteRegex,
28 pub named_regex: String,
29 pub route_keys: FxHashMap<String, String>,
30}
31
32#[derive(Debug)]
33pub struct NamedMiddlewareRegex {
34 pub named_regex: String,
35}
36
37struct ParsedParameter {
38 key: String,
39 repeat: bool,
40 optional: bool,
41}
42
43fn parse_parameter(param: &str) -> ParsedParameter {
50 let mut key = param.to_string();
51 let optional = key.starts_with('[') && key.ends_with(']');
52 if optional {
53 key = key[1..key.len() - 1].to_string();
54 }
55 let repeat = key.starts_with("...");
56 if repeat {
57 key = key[3..].to_string();
58 }
59 ParsedParameter {
60 key,
61 repeat,
62 optional,
63 }
64}
65
66fn escape_string_regexp(segment: &str) -> String {
67 regex::escape(segment)
68}
69
70fn remove_trailing_slash(route: &str) -> &str {
76 if route == "/" {
77 route
78 } else {
79 route.trim_end_matches('/')
80 }
81}
82
83static PARAM_MATCH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
84
85fn get_parametrized_route(route: &str) -> (String, FxHashMap<String, Group>) {
86 let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
87 let mut groups: FxHashMap<String, Group> = FxHashMap::default();
88 let mut group_index = 1;
89 let parameterized_route = segments
90 .iter()
91 .map(|segment| {
92 let marker_match = INTERCEPTION_ROUTE_MARKERS
93 .iter()
94 .find(|&&m| segment.starts_with(m))
95 .copied();
96 let param_matches = PARAM_MATCH_REGEX.captures(segment);
97 if let Some(matches) = param_matches {
98 let ParsedParameter {
99 key,
100 optional,
101 repeat,
102 } = parse_parameter(&matches[1]);
103 groups.insert(
104 key,
105 Group {
106 pos: group_index,
107 repeat,
108 optional,
109 },
110 );
111 group_index += 1;
112 if let Some(marker) = marker_match {
113 return format!("/{}([^/]+?)", escape_string_regexp(marker));
114 } else {
115 return match (repeat, optional) {
116 (true, true) => "(?:/(.+?))?",
117 (true, false) => "/(.+?)",
118 (false, true) => "(?:/([^/]+?))?",
119 (false, false) => "/([^/]+?)",
120 }
121 .to_string();
122 }
123 }
124 format!("/{}", escape_string_regexp(segment))
125 })
126 .collect::<Vec<String>>()
127 .join("");
128 (parameterized_route, groups)
129}
130
131pub fn get_route_regex(normalized_route: &str) -> RouteRegex {
135 let (parameterized_route, groups) = get_parametrized_route(normalized_route);
136 RouteRegex {
137 regex: format!("^{parameterized_route}(?:/)?$"),
138 groups,
139 }
140}
141
142fn build_get_safe_route_key() -> impl FnMut() -> String {
145 let mut i = 0;
146
147 move || {
148 let mut route_key = String::new();
149 i += 1;
150 let mut j = i;
151
152 while j > 0 {
153 route_key.push((97 + ((j - 1) % 26)) as u8 as char);
154 j = (j - 1) / 26;
155 }
156
157 i += 1;
158 route_key
159 }
160}
161
162fn get_safe_key_from_segment(
163 get_safe_route_key: &mut impl FnMut() -> String,
164 segment: &str,
165 route_keys: &mut FxHashMap<String, String>,
166 key_prefix: Option<&'static str>,
167) -> String {
168 let ParsedParameter {
169 key,
170 optional,
171 repeat,
172 } = parse_parameter(segment);
173
174 let mut cleaned_key = key.replace(|c: char| !c.is_alphanumeric(), "");
177 if let Some(prefix) = key_prefix {
178 cleaned_key = format!("{prefix}{cleaned_key}");
179 }
180 let mut invalid_key = false;
181
182 if cleaned_key.is_empty() || cleaned_key.len() > 30 {
185 invalid_key = true;
186 }
187 if cleaned_key.chars().next().unwrap().is_numeric() {
188 invalid_key = true;
189 }
190 if invalid_key {
191 cleaned_key = get_safe_route_key();
192 }
193 if let Some(prefix) = key_prefix {
194 route_keys.insert(cleaned_key.clone(), format!("{prefix}{key}"));
195 } else {
196 route_keys.insert(cleaned_key.clone(), key);
197 }
198 match (repeat, optional) {
199 (true, true) => format!(r"(?:/(?P<{cleaned_key}>.+?))?"),
200 (true, false) => format!(r"/(?P<{cleaned_key}>.+?)"),
201 (false, true) => format!(r"(?:/(?P<{cleaned_key}>[^/]+?))?"),
202 (false, false) => format!(r"/(?P<{cleaned_key}>[^/]+?)"),
203 }
204}
205
206fn get_named_parametrized_route(
207 route: &str,
208 prefix_route_keys: bool,
209) -> (String, FxHashMap<String, String>) {
210 let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
211 let get_safe_route_key = &mut build_get_safe_route_key();
212 let mut route_keys: FxHashMap<String, String> = FxHashMap::default();
213 let parameterized_route = segments
214 .iter()
215 .map(|segment| {
216 let key_prefix = if prefix_route_keys {
217 let has_interception_marker = INTERCEPTION_ROUTE_MARKERS
218 .iter()
219 .any(|&m| segment.starts_with(m));
220 if has_interception_marker {
221 Some(NEXT_INTERCEPTION_MARKER_PREFIX)
222 } else {
223 Some(NEXT_QUERY_PARAM_PREFIX)
224 }
225 } else {
226 None
227 };
228 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
229 let param_matches = RE.captures(segment);
230 if let Some(matches) = param_matches {
231 return get_safe_key_from_segment(
232 get_safe_route_key,
233 &matches[1],
234 &mut route_keys,
235 key_prefix,
236 );
237 }
238 format!("/{}", escape_string_regexp(segment))
239 })
240 .collect::<Vec<String>>()
241 .join("");
242 (parameterized_route, route_keys)
243}
244
245pub fn get_named_route_regex(normalized_route: &str) -> NamedRouteRegex {
253 let (parameterized_route, route_keys) = get_named_parametrized_route(normalized_route, false);
254 let regex = get_route_regex(normalized_route);
255 NamedRouteRegex {
256 regex,
257 named_regex: format!("^{parameterized_route}(?:/)?$"),
258 route_keys,
259 }
260}
261
262pub fn get_named_middleware_regex(normalized_route: &str) -> String {
265 let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true);
266 format!("^{parameterized_route}(?:/)?$")
267}