1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt;
4
5pub fn expand(
6 template: &str,
7 substitutions: &HashMap<String, Value>,
8) -> Result<String, StdUriTemplateError> {
9 expand_impl(template, substitutions)
10}
11
12#[derive(Debug, Clone)]
13pub enum Value {
14 String(String),
15 Bool(bool),
16 Integer(i64),
17 Float(f64),
18 List(Vec<Value>),
19 Map(Vec<(String, Value)>),
20}
21
22#[derive(Debug)]
23pub struct StdUriTemplateError {
24 message: String,
25}
26
27impl fmt::Display for StdUriTemplateError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "{}", self.message)
30 }
31}
32
33impl std::error::Error for StdUriTemplateError {}
34
35impl StdUriTemplateError {
36 fn new(message: String) -> Self {
37 StdUriTemplateError { message }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq)]
42enum Operator {
43 NoOp,
44 Plus,
45 Hash,
46 Dot,
47 Slash,
48 Semicolon,
49 QuestionMark,
50 Amp,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq)]
54enum SubstitutionType {
55 Empty,
56 String,
57 List,
58 Map,
59}
60
61fn validate_literal(c: char, col: usize) -> Result<(), StdUriTemplateError> {
62 match c {
63 '+' | '#' | '/' | ';' | '?' | '&' | ' ' | '!' | '=' | '$' | '|' | '*' | ':' | '~'
64 | '-' => Err(StdUriTemplateError::new(format!(
65 "Illegal character identified in the token at col:{}",
66 col
67 ))),
68 _ => Ok(()),
69 }
70}
71
72fn get_max_char(buffer: &str, col: usize) -> Result<i32, StdUriTemplateError> {
73 if buffer.is_empty() {
74 return Ok(-1);
75 }
76
77 let value = buffer.parse::<i32>().map_err(|_| {
78 StdUriTemplateError::new(format!("Cannot parse max chars at col:{}", col))
79 })?;
80
81 if buffer.starts_with('0') {
82 return Err(StdUriTemplateError::new(format!(
83 "Cannot parse max chars at col:{}",
84 col
85 )));
86 }
87
88 if value < 1 || value > 9999 {
89 return Err(StdUriTemplateError::new(format!(
90 "Cannot parse max chars at col:{}",
91 col
92 )));
93 }
94
95 Ok(value)
96}
97
98fn get_operator(
99 c: char,
100 token: &mut String,
101 col: usize,
102) -> Result<Operator, StdUriTemplateError> {
103 match c {
104 '+' => Ok(Operator::Plus),
105 '#' => Ok(Operator::Hash),
106 '.' => Ok(Operator::Dot),
107 '/' => Ok(Operator::Slash),
108 ';' => Ok(Operator::Semicolon),
109 '?' => Ok(Operator::QuestionMark),
110 '&' => Ok(Operator::Amp),
111 _ => {
112 validate_literal(c, col)?;
113 token.push(c);
114 Ok(Operator::NoOp)
115 }
116 }
117}
118
119fn expand_impl(
120 template: &str,
121 substitutions: &HashMap<String, Value>,
122) -> Result<String, StdUriTemplateError> {
123 let mut result = String::with_capacity(template.len() * 2);
124
125 let mut to_token = false;
126 let mut token = String::new();
127
128 let mut operator: Option<Operator> = None;
129 let mut composite = false;
130 let mut to_max_char_buffer = false;
131 let mut max_char_buffer = String::with_capacity(3);
132 let mut first_token = true;
133
134 for (i, character) in template.chars().enumerate() {
135 match character {
136 '{' => {
137 to_token = true;
138 token.clear();
139 first_token = true;
140 }
141 '}' => {
142 if to_token {
143 if to_max_char_buffer && max_char_buffer.is_empty() {
144 return Err(StdUriTemplateError::new(format!(
145 "Found an empty prefix at col:{}",
146 i
147 )));
148 }
149 let max_char = get_max_char(&max_char_buffer, i)?;
150 let expanded = expand_token(
151 operator.unwrap_or(Operator::NoOp),
152 &token,
153 composite,
154 max_char,
155 first_token,
156 substitutions,
157 &mut result,
158 i,
159 )?;
160 if expanded && first_token {
161 first_token = false;
162 }
163 to_token = false;
164 token.clear();
165 operator = None;
166 composite = false;
167 to_max_char_buffer = false;
168 max_char_buffer.clear();
169 } else {
170 return Err(StdUriTemplateError::new(format!(
171 "Failed to expand token, invalid at col:{}",
172 i
173 )));
174 }
175 }
176 ',' if to_token => {
177 if to_max_char_buffer && max_char_buffer.is_empty() {
178 return Err(StdUriTemplateError::new(format!(
179 "Found an empty prefix at col:{}",
180 i
181 )));
182 }
183 let max_char = get_max_char(&max_char_buffer, i)?;
184 let expanded = expand_token(
185 operator.unwrap_or(Operator::NoOp),
186 &token,
187 composite,
188 max_char,
189 first_token,
190 substitutions,
191 &mut result,
192 i,
193 )?;
194 if expanded && first_token {
195 first_token = false;
196 }
197 token.clear();
198 composite = false;
199 to_max_char_buffer = false;
200 max_char_buffer.clear();
201 }
202 _ => {
203 if to_token {
204 if operator.is_none() {
205 operator = Some(get_operator(character, &mut token, i)?);
206 } else if to_max_char_buffer {
207 if character.is_ascii_digit() {
208 max_char_buffer.push(character);
209 } else {
210 return Err(StdUriTemplateError::new(format!(
211 "Illegal character identified in the token at col:{}",
212 i
213 )));
214 }
215 } else {
216 match character {
217 ':' => {
218 to_max_char_buffer = true;
219 max_char_buffer.clear();
220 }
221 '*' => {
222 composite = true;
223 }
224 _ => {
225 validate_literal(character, i)?;
226 token.push(character);
227 }
228 }
229 }
230 } else {
231 if (character as u32) > 0x7F {
232 let mut buf = [0u8; 4];
233 let encoded = character.encode_utf8(&mut buf);
234 for b in encoded.bytes() {
235 use std::fmt::Write;
236 write!(result, "%{:02X}", b).unwrap();
237 }
238 } else {
239 result.push(character);
240 }
241 }
242 }
243 }
244 }
245
246 if !to_token {
247 Ok(result)
248 } else {
249 Err(StdUriTemplateError::new("Unterminated token".to_string()))
250 }
251}
252
253fn add_prefix(op: Operator, result: &mut String) {
254 match op {
255 Operator::Hash => result.push('#'),
256 Operator::Dot => result.push('.'),
257 Operator::Slash => result.push('/'),
258 Operator::Semicolon => result.push(';'),
259 Operator::QuestionMark => result.push('?'),
260 Operator::Amp => result.push('&'),
261 _ => {}
262 }
263}
264
265fn add_separator(op: Operator, result: &mut String) {
266 match op {
267 Operator::Dot => result.push('.'),
268 Operator::Slash => result.push('/'),
269 Operator::Semicolon => result.push(';'),
270 Operator::QuestionMark | Operator::Amp => result.push('&'),
271 _ => result.push(','),
272 }
273}
274
275fn add_value(op: Operator, token: &str, value: &str, result: &mut String, max_char: i32) {
276 match op {
277 Operator::Plus | Operator::Hash => {
278 add_expanded_value(None, value, result, max_char, false);
279 }
280 Operator::QuestionMark | Operator::Amp => {
281 result.push_str(token);
282 result.push('=');
283 add_expanded_value(None, value, result, max_char, true);
284 }
285 Operator::Semicolon => {
286 result.push_str(token);
287 add_expanded_value(Some("="), value, result, max_char, true);
288 }
289 Operator::Dot | Operator::Slash | Operator::NoOp => {
290 add_expanded_value(None, value, result, max_char, true);
291 }
292 }
293}
294
295fn add_value_element(op: Operator, _token: &str, value: &str, result: &mut String, max_char: i32) {
296 match op {
297 Operator::Plus | Operator::Hash => {
298 add_expanded_value(None, value, result, max_char, false);
299 }
300 Operator::QuestionMark
301 | Operator::Amp
302 | Operator::Semicolon
303 | Operator::Dot
304 | Operator::Slash
305 | Operator::NoOp => {
306 add_expanded_value(None, value, result, max_char, true);
307 }
308 }
309}
310
311fn is_iprivate(cp: char) -> bool {
312 (0xE000..=0xF8FF).contains(&(cp as u32))
313}
314
315fn is_ucschar(cp: char) -> bool {
316 let code = cp as u32;
317 (0xA0..=0xD7FF).contains(&code)
318 || (0xF900..=0xFDCF).contains(&code)
319 || (0xFDF0..=0xFFEF).contains(&code)
320}
321
322fn is_unreserved(c: char) -> bool {
323 c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~'
324}
325
326fn percent_encode_char(c: char, result: &mut String) {
327 let mut buf = [0u8; 4];
328 let encoded = c.encode_utf8(&mut buf);
329 for byte in encoded.as_bytes() {
330 result.push('%');
331 result.push(to_hex_digit(byte >> 4));
332 result.push(to_hex_digit(byte & 0x0F));
333 }
334}
335
336fn url_encode_char(c: char, result: &mut String) {
337 if is_unreserved(c) {
338 result.push(c);
339 } else {
340 percent_encode_char(c, result);
341 }
342}
343
344fn to_hex_digit(nibble: u8) -> char {
345 match nibble {
346 0..=9 => (b'0' + nibble) as char,
347 10..=15 => (b'A' + nibble - 10) as char,
348 _ => unreachable!(),
349 }
350}
351
352fn add_expanded_value(
353 prefix: Option<&str>,
354 value: &str,
355 result: &mut String,
356 max_char: i32,
357 replace_reserved: bool,
358) {
359 let max = if max_char != -1 {
360 max_char as usize
361 } else {
362 usize::MAX
363 };
364
365 let mut to_reserved = false;
366 let mut reserved_buffer = String::with_capacity(3);
367 let mut to_append = String::with_capacity(12);
368 let mut prefix_pending = prefix;
369
370 for character in value.chars().take(max) {
371 if let Some(p) = prefix_pending.take() {
372 result.push_str(p);
373 }
374
375 if character == '%' && !replace_reserved {
376 to_reserved = true;
377 reserved_buffer.clear();
378 }
379
380 to_append.clear();
381 if replace_reserved || is_ucschar(character) || is_iprivate(character) {
382 url_encode_char(character, &mut to_append);
383 } else if !character.is_ascii() {
384 percent_encode_char(character, &mut to_append);
385 } else {
386 to_append.push(character);
387 }
388
389 if to_reserved {
390 reserved_buffer.push_str(&to_append);
391
392 if reserved_buffer.len() == 3 {
393 let is_encoded = is_valid_percent_encoded(&reserved_buffer);
394
395 if is_encoded {
396 result.push_str(&reserved_buffer);
397 } else {
398 result.push_str("%25");
399 result.push_str(&reserved_buffer[1..]);
400 }
401 to_reserved = false;
402 reserved_buffer.clear();
403 }
404 } else if character == ' ' {
405 result.push_str("%20");
406 } else if character == '%' {
407 result.push_str("%25");
408 } else {
409 result.push_str(&to_append);
410 }
411 }
412
413 if to_reserved {
414 result.push_str("%25");
415 result.push_str(&reserved_buffer[1..]);
416 }
417}
418
419fn is_valid_percent_encoded(s: &str) -> bool {
420 let b = s.as_bytes();
421 b.len() == 3 && b[0] == b'%' && b[1].is_ascii_hexdigit() && b[2].is_ascii_hexdigit()
422}
423
424fn get_substitution_type(
425 value: Option<&Value>,
426 _col: usize,
427) -> Result<SubstitutionType, StdUriTemplateError> {
428 match value {
429 None => Ok(SubstitutionType::Empty),
430 Some(v) => match v {
431 Value::String(_) | Value::Bool(_) | Value::Integer(_) | Value::Float(_) => {
432 Ok(SubstitutionType::String)
433 }
434 Value::List(_) => Ok(SubstitutionType::List),
435 Value::Map(_) => Ok(SubstitutionType::Map),
436 },
437 }
438}
439
440fn is_empty(subst_type: SubstitutionType, value: &Value) -> bool {
441 match subst_type {
442 SubstitutionType::String => false,
443 SubstitutionType::List => {
444 if let Value::List(l) = value {
445 l.is_empty()
446 } else {
447 true
448 }
449 }
450 SubstitutionType::Map => {
451 if let Value::Map(m) = value {
452 m.is_empty()
453 } else {
454 true
455 }
456 }
457 SubstitutionType::Empty => true,
458 }
459}
460
461fn convert_native_types(value: &Value) -> Result<Cow<'_, str>, StdUriTemplateError> {
462 match value {
463 Value::String(s) => Ok(Cow::Borrowed(s)),
464 Value::Bool(b) => Ok(Cow::Owned(b.to_string())),
465 Value::Integer(i) => Ok(Cow::Owned(i.to_string())),
466 Value::Float(f) => {
467 if *f == (*f as i64) as f64 && f.is_finite() {
468 Ok(Cow::Owned((*f as i64).to_string()))
469 } else {
470 Ok(Cow::Owned(f.to_string()))
471 }
472 }
473 Value::List(_) | Value::Map(_) => Err(StdUriTemplateError::new(format!(
474 "Illegal class passed as substitution, found {:?}",
475 value
476 ))),
477 }
478}
479
480fn check_varname(token: &str, col: usize) -> Result<(), StdUriTemplateError> {
481 if token.starts_with('.') || token.ends_with('.') {
482 return Err(StdUriTemplateError::new(format!(
483 "Invalid variable name (leading/trailing dot) at col:{}",
484 col
485 )));
486 }
487 if token.contains("..") {
488 return Err(StdUriTemplateError::new(format!(
489 "Invalid variable name (consecutive dots) at col:{}",
490 col
491 )));
492 }
493 let bytes = token.as_bytes();
494 for i in 0..bytes.len() {
495 if bytes[i] == b'%' {
496 if i + 2 < bytes.len() && bytes[i + 1].is_ascii_hexdigit() && bytes[i + 2].is_ascii_hexdigit()
497 {
498 } else {
500 return Err(StdUriTemplateError::new(format!(
501 "Invalid percent-encoding in variable name at col:{}",
502 col
503 )));
504 }
505 }
506 }
507 Ok(())
508}
509
510#[allow(clippy::too_many_arguments)]
511fn expand_token(
512 operator: Operator,
513 token: &str,
514 composite: bool,
515 max_char: i32,
516 first_token: bool,
517 substitutions: &HashMap<String, Value>,
518 result: &mut String,
519 col: usize,
520) -> Result<bool, StdUriTemplateError> {
521 if token.is_empty() {
522 return Err(StdUriTemplateError::new(format!(
523 "Found an empty token at col:{}",
524 col
525 )));
526 }
527
528 check_varname(token, col)?;
529
530 let value = substitutions.get(token);
531 let subst_type = get_substitution_type(value, col)?;
532 if subst_type == SubstitutionType::Empty {
533 return Ok(false);
534 }
535
536 let value = value.unwrap();
537 if is_empty(subst_type, value) {
538 return Ok(false);
539 }
540
541 if first_token {
542 add_prefix(operator, result);
543 } else {
544 add_separator(operator, result);
545 }
546
547 match subst_type {
548 SubstitutionType::String => {
549 add_string_value(operator, token, value, result, max_char)?;
550 }
551 SubstitutionType::List => {
552 add_list_value(operator, token, value, result, max_char, composite)?;
553 }
554 SubstitutionType::Map => {
555 add_map_value(operator, token, value, result, max_char, composite)?;
556 }
557 SubstitutionType::Empty => {}
558 }
559
560 Ok(true)
561}
562
563fn add_string_value(
564 operator: Operator,
565 token: &str,
566 value: &Value,
567 result: &mut String,
568 max_char: i32,
569) -> Result<(), StdUriTemplateError> {
570 let s = convert_native_types(value)?;
571 add_value(operator, token, &s, result, max_char);
572 Ok(())
573}
574
575fn add_list_value(
576 operator: Operator,
577 token: &str,
578 value: &Value,
579 result: &mut String,
580 max_char: i32,
581 composite: bool,
582) -> Result<(), StdUriTemplateError> {
583 if let Value::List(list) = value {
584 let mut first = true;
585 for v in list {
586 let s = convert_native_types(v)?;
587 if first {
588 add_value(operator, token, &s, result, max_char);
589 first = false;
590 } else if composite {
591 add_separator(operator, result);
592 add_value(operator, token, &s, result, max_char);
593 } else {
594 result.push(',');
595 add_value_element(operator, token, &s, result, max_char);
596 }
597 }
598 }
599 Ok(())
600}
601
602fn add_map_value(
603 operator: Operator,
604 token: &str,
605 value: &Value,
606 result: &mut String,
607 max_char: i32,
608 composite: bool,
609) -> Result<(), StdUriTemplateError> {
610 if max_char != -1 {
611 return Err(StdUriTemplateError::new(
612 "Value trimming is not allowed on Maps".to_string(),
613 ));
614 }
615
616 if let Value::Map(map) = value {
617 let mut first = true;
618 for (key, val) in map {
619 let v = convert_native_types(val)?;
620 if composite {
621 if !first {
622 add_separator(operator, result);
623 }
624 add_value_element(operator, token, key, result, max_char);
625 result.push('=');
626 } else {
627 if first {
628 add_value(operator, token, key, result, max_char);
629 } else {
630 result.push(',');
631 add_value_element(operator, token, key, result, max_char);
632 }
633 result.push(',');
634 }
635 add_value_element(operator, token, &v, result, max_char);
636 first = false;
637 }
638 }
639
640 Ok(())
641}