1use std::fmt;
26
27const MAX_MATCH_LEN: usize = 8 * 1024;
29
30#[derive(Clone, PartialEq, Eq)]
32pub struct UriTemplate {
33 source: String,
34 parts: Vec<Part>,
35 query: Vec<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39enum Part {
40 Literal(String),
41 Var { name: String, op: Op },
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45enum Op {
46 Simple,
48 Reserved,
50 Path,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56#[error("invalid URI template `{template}`: {reason}")]
57pub struct UriTemplateError {
58 template: String,
59 reason: String,
60}
61
62impl UriTemplate {
63 pub fn parse(template: &str) -> Result<Self, UriTemplateError> {
70 let fail = |reason: &str| UriTemplateError {
71 template: template.to_owned(),
72 reason: reason.to_owned(),
73 };
74 if template.is_empty() {
75 return Err(fail("the template is empty"));
76 }
77 let mut parts = Vec::new();
78 let mut query: Vec<String> = Vec::new();
79 let mut names: Vec<String> = Vec::new();
80 let mut rest = template;
81 while !rest.is_empty() {
82 if let Some(after_brace) = rest.strip_prefix('{') {
83 let end = after_brace.find('}').ok_or_else(|| fail("unclosed `{`"))?;
84 let (expr, tail) = after_brace.split_at(end);
85 rest = tail.get(1..).unwrap_or_default();
86 let (op, list) = match expr.chars().next() {
87 Some('+') => (Some(Op::Reserved), expr.get(1..).unwrap_or_default()),
88 Some('/') => (Some(Op::Path), expr.get(1..).unwrap_or_default()),
89 Some('?' | '&') => (None, expr.get(1..).unwrap_or_default()),
90 Some('#' | '.' | ';' | '=' | ',' | '!' | '@' | '|') => {
91 return Err(fail(
92 "only `{var}`, `{+var}`, `{/var}` and `{?var}` expressions are supported",
93 ));
94 }
95 _ => (Some(Op::Simple), expr),
96 };
97 let vars: Vec<&str> = list.split(',').collect();
98 for var in &vars {
99 if var.ends_with('*') || var.contains(':') {
100 return Err(fail("variable modifiers (`*`, `:n`) are not supported"));
101 }
102 if !is_var_name(var) {
103 return Err(fail(
104 "variable names must be non-empty and use only A-Z, a-z, 0-9 and `_`",
105 ));
106 }
107 if names.iter().any(|n| n == var) {
108 return Err(fail("a variable appears twice"));
109 }
110 names.push((*var).to_owned());
111 }
112 match op {
113 None => query.extend(vars.iter().map(|v| (*v).to_owned())),
114 Some(op) => {
115 if !query.is_empty() {
116 return Err(fail("query expressions must come last"));
117 }
118 let [name] = vars.as_slice() else {
119 return Err(fail(
120 "path expressions take exactly one variable; use one expression per variable",
121 ));
122 };
123 if let Some(Part::Var { op: prev, .. }) = parts.last()
124 && !(op == Op::Path && *prev != Op::Reserved)
125 {
126 return Err(fail(
127 "two variables must be separated by a literal (or use `{/var}`)",
128 ));
129 }
130 if op == Op::Reserved
131 && parts.iter().any(|p| {
132 matches!(
133 p,
134 Part::Var {
135 op: Op::Reserved,
136 ..
137 }
138 )
139 })
140 {
141 return Err(fail("at most one `{+var}` expression is supported"));
142 }
143 parts.push(Part::Var {
144 name: (*name).to_owned(),
145 op,
146 });
147 }
148 }
149 } else {
150 let end = rest.find(['{', '}']).unwrap_or(rest.len());
151 if rest.starts_with('}') {
152 return Err(fail("unmatched `}`"));
153 }
154 let (literal, tail) = rest.split_at(end);
155 if literal.chars().any(|c| c.is_control() || c == ' ') {
156 return Err(fail("literals cannot contain spaces or control characters"));
157 }
158 if !query.is_empty() {
159 return Err(fail("query expressions must come last"));
160 }
161 parts.push(Part::Literal(literal.to_owned()));
162 rest = tail;
163 }
164 }
165 if !matches!(parts.first(), Some(Part::Literal(_))) {
166 return Err(fail(
167 "the template must start with a literal scheme, e.g. `file:///{path}`",
168 ));
169 }
170 Ok(Self {
171 source: template.to_owned(),
172 parts,
173 query,
174 })
175 }
176
177 #[must_use]
179 pub fn as_str(&self) -> &str {
180 &self.source
181 }
182
183 pub fn variables(&self) -> impl Iterator<Item = &str> {
185 self.parts
186 .iter()
187 .filter_map(|p| match p {
188 Part::Var { name, .. } => Some(name.as_str()),
189 Part::Literal(_) => None,
190 })
191 .chain(self.query.iter().map(String::as_str))
192 }
193
194 #[must_use]
196 pub fn matches(&self, uri: &str) -> Option<UriVars> {
197 if uri.len() > MAX_MATCH_LEN {
198 return None;
199 }
200 let (path, query) = if self.query.is_empty() {
201 (uri, None)
202 } else {
203 match uri.split_once('?') {
204 Some((path, query)) => (path, Some(query)),
205 None => (uri, None),
206 }
207 };
208 let mut vars = Vec::new();
209 let mut rest = path;
210 let mut parts = self.parts.iter().peekable();
211 while let Some(part) = parts.next() {
212 match part {
213 Part::Literal(literal) => rest = rest.strip_prefix(literal.as_str())?,
214 Part::Var { name, op } => {
215 if *op == Op::Path {
216 rest = rest.strip_prefix('/')?;
217 }
218 let forbidden: &[char] = match op {
219 Op::Reserved => &['?', '#'],
220 Op::Simple | Op::Path => &['/', '?', '#'],
221 };
222 let end = match parts.peek() {
223 None => rest.len(),
224 Some(Part::Literal(next)) => match op {
225 Op::Reserved => rest.rfind(next.as_str())?,
226 Op::Simple | Op::Path => rest.find(next.as_str())?,
227 },
228 Some(Part::Var { .. }) => rest.find('/').unwrap_or(rest.len()),
230 };
231 let (raw, tail) = rest.split_at(end);
232 if raw.is_empty() || raw.contains(forbidden) {
233 return None;
234 }
235 vars.push((name.clone(), decode_value(raw, *op != Op::Reserved)?));
236 rest = tail;
237 }
238 }
239 }
240 if !rest.is_empty() {
241 return None;
242 }
243 if let Some(query) = query {
244 for pair in query.split('&').filter(|p| !p.is_empty()) {
245 let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
246 if let Some(name) = self.query.iter().find(|n| n.as_str() == key)
247 && !vars.iter().any(|(n, _)| n == name)
248 {
249 vars.push((name.clone(), decode_value(value, true)?));
250 }
251 }
252 }
253 Some(UriVars { vars })
254 }
255}
256
257impl fmt::Debug for UriTemplate {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.debug_tuple("UriTemplate").field(&self.source).finish()
260 }
261}
262
263impl fmt::Display for UriTemplate {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.write_str(&self.source)
266 }
267}
268
269#[derive(Debug, Clone, Default, PartialEq, Eq)]
271pub struct UriVars {
272 vars: Vec<(String, String)>,
273}
274
275impl UriVars {
276 #[must_use]
278 pub fn get(&self, name: &str) -> Option<&str> {
279 self.vars
280 .iter()
281 .find(|(n, _)| n == name)
282 .map(|(_, v)| v.as_str())
283 }
284
285 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
287 self.vars.iter().map(|(n, v)| (n.as_str(), v.as_str()))
288 }
289}
290
291fn is_var_name(name: &str) -> bool {
292 !name.is_empty() && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
293}
294
295fn decode_value(raw: &str, single_segment: bool) -> Option<String> {
298 let value = percent_decode(raw)?;
299 let forbidden = |c: char| c.is_control() || (single_segment && matches!(c, '/' | '\\'));
300 (!value.contains(forbidden)).then_some(value)
301}
302
303fn percent_decode(input: &str) -> Option<String> {
305 if !input.contains('%') {
306 return Some(input.to_owned());
307 }
308 let bytes = input.as_bytes();
309 let mut out = Vec::with_capacity(bytes.len());
310 let mut i = 0;
311 while let Some(&b) = bytes.get(i) {
312 if b == b'%' {
313 let hi = hex(*bytes.get(i + 1)?)?;
314 let lo = hex(*bytes.get(i + 2)?)?;
315 out.push(hi << 4 | lo);
316 i += 3;
317 } else {
318 out.push(b);
319 i += 1;
320 }
321 }
322 String::from_utf8(out).ok()
323}
324
325fn hex(b: u8) -> Option<u8> {
326 match b {
327 b'0'..=b'9' => Some(b - b'0'),
328 b'a'..=b'f' => Some(b - b'a' + 10),
329 b'A'..=b'F' => Some(b - b'A' + 10),
330 _ => None,
331 }
332}
333
334pub(crate) fn is_absolute_uri(uri: &str) -> bool {
337 let Some((scheme, rest)) = uri.split_once(':') else {
338 return false;
339 };
340 let mut chars = scheme.chars();
341 chars.next().is_some_and(|c| c.is_ascii_alphabetic())
342 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
343 && !rest.is_empty()
344 && !uri.chars().any(|c| c.is_control() || c.is_whitespace())
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 fn vars(template: &str, uri: &str) -> Option<Vec<(String, String)>> {
352 UriTemplate::parse(template)
353 .unwrap()
354 .matches(uri)
355 .map(|v| v.vars)
356 }
357
358 fn pairs(list: &[(&str, &str)]) -> Option<Vec<(String, String)>> {
359 Some(
360 list.iter()
361 .map(|(a, b)| ((*a).to_owned(), (*b).to_owned()))
362 .collect(),
363 )
364 }
365
366 #[test]
367 fn simple_variables() {
368 assert_eq!(
369 vars("test://template/{id}/data", "test://template/123/data"),
370 pairs(&[("id", "123")])
371 );
372 assert_eq!(
373 vars("test://template/{id}/data", "test://template//data"),
374 None
375 );
376 assert_eq!(
377 vars("test://template/{id}/data", "test://template/1/2/data"),
378 None
379 );
380 assert_eq!(
381 vars("db://{table}/{id}", "db://users/a%20b"),
382 pairs(&[("table", "users"), ("id", "a b")])
383 );
384 assert_eq!(vars("db://{table}/{id}", "db://users/%zz"), None);
385 assert_eq!(
386 vars("file:///{name}.txt", "file:///a.b.txt"),
387 pairs(&[("name", "a.b")])
388 );
389 assert_eq!(
390 vars("file:///{name}.txt", "file:///a.txt.txt"),
391 None,
392 "a simple variable ends at the first occurrence of the next literal"
393 );
394 }
395
396 #[test]
397 fn reserved_and_path_variables() {
398 assert_eq!(
399 vars("file:///{+path}", "file:///src/main.rs"),
400 pairs(&[("path", "src/main.rs")])
401 );
402 assert_eq!(
403 vars("file:///{+path}.meta", "file:///a.b/c.meta"),
404 pairs(&[("path", "a.b/c")])
405 );
406 assert_eq!(
407 vars("repo://{owner}{/name}", "repo://rust-lang/rust"),
408 pairs(&[("owner", "rust-lang"), ("name", "rust")])
409 );
410 }
411
412 #[test]
413 fn query_variables_are_optional_and_unordered() {
414 let t = "search://items{?q,limit}";
415 assert_eq!(
416 vars(t, "search://items?limit=5&q=a%2Bb&other=1"),
417 pairs(&[("limit", "5"), ("q", "a+b")])
418 );
419 assert_eq!(vars(t, "search://items"), pairs(&[]));
420 let parsed = UriTemplate::parse(t).unwrap();
421 assert_eq!(parsed.variables().collect::<Vec<_>>(), ["q", "limit"]);
422 }
423
424 #[test]
425 fn unsupported_templates_are_rejected() {
426 for bad in [
427 "",
428 "{x}",
429 "a://{x",
430 "a://x}",
431 "a://{x}{y}",
432 "a://{+x}/{+y}",
433 "a://{x*}",
434 "a://{x:3}",
435 "a://{#x}",
436 "a://{x,y}",
437 "a://{x}/{x}",
438 "a://{?q}/tail",
439 "a://{bad-name}",
440 "a:// {x}",
441 ] {
442 assert!(
443 UriTemplate::parse(bad).is_err(),
444 "{bad:?} should be rejected"
445 );
446 }
447 }
448
449 #[test]
450 fn decoded_values_are_rechecked() {
451 let t = "file:///docs/{name}";
452 for uri in [
453 "file:///docs/..%2Fsecret",
454 "file:///docs/..%5Csecret",
455 "file:///docs/..\\secret",
456 "file:///docs/a%00b",
457 "file:///docs/a%0Ab",
458 ] {
459 assert_eq!(vars(t, uri), None, "{uri}");
460 }
461 assert_eq!(vars(t, "file:///docs/a%20b"), pairs(&[("name", "a b")]));
462 assert_eq!(vars("q://x{?q}", "q://x?q=a%2Fb"), None);
463 assert_eq!(
465 vars("file:///{+path}", "file:///a%2Fb/c"),
466 pairs(&[("path", "a/b/c")])
467 );
468 assert_eq!(vars("file:///{+path}", "file:///a%00"), None);
469 }
470
471 #[test]
472 fn long_uris_never_match() {
473 let uri = format!("file:///{}", "a".repeat(MAX_MATCH_LEN));
474 assert_eq!(vars("file:///{+path}", &uri), None);
475 }
476
477 #[test]
478 fn absolute_uris() {
479 assert!(is_absolute_uri("test://static-text"));
480 assert!(is_absolute_uri("urn:isbn:0451450523"));
481 assert!(!is_absolute_uri("no-scheme"));
482 assert!(!is_absolute_uri("1http://x"));
483 assert!(!is_absolute_uri("http://a b"));
484 assert!(!is_absolute_uri("http:"));
485 }
486}