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 buffer.parse::<i32>().map_err(|_| {
78 StdUriTemplateError::new(format!("Cannot parse max chars at col:{}", col))
79 })
80}
81
82fn get_operator(
83 c: char,
84 token: &mut String,
85 col: usize,
86) -> Result<Operator, StdUriTemplateError> {
87 match c {
88 '+' => Ok(Operator::Plus),
89 '#' => Ok(Operator::Hash),
90 '.' => Ok(Operator::Dot),
91 '/' => Ok(Operator::Slash),
92 ';' => Ok(Operator::Semicolon),
93 '?' => Ok(Operator::QuestionMark),
94 '&' => Ok(Operator::Amp),
95 _ => {
96 validate_literal(c, col)?;
97 token.push(c);
98 Ok(Operator::NoOp)
99 }
100 }
101}
102
103fn expand_impl(
104 template: &str,
105 substitutions: &HashMap<String, Value>,
106) -> Result<String, StdUriTemplateError> {
107 let mut result = String::with_capacity(template.len() * 2);
108
109 let mut to_token = false;
110 let mut token = String::new();
111
112 let mut operator: Option<Operator> = None;
113 let mut composite = false;
114 let mut to_max_char_buffer = false;
115 let mut max_char_buffer = String::with_capacity(3);
116 let mut first_token = true;
117
118 for (i, character) in template.chars().enumerate() {
119 match character {
120 '{' => {
121 to_token = true;
122 token.clear();
123 first_token = true;
124 }
125 '}' => {
126 if to_token {
127 let max_char = get_max_char(&max_char_buffer, i)?;
128 let expanded = expand_token(
129 operator.unwrap_or(Operator::NoOp),
130 &token,
131 composite,
132 max_char,
133 first_token,
134 substitutions,
135 &mut result,
136 i,
137 )?;
138 if expanded && first_token {
139 first_token = false;
140 }
141 to_token = false;
142 token.clear();
143 operator = None;
144 composite = false;
145 to_max_char_buffer = false;
146 max_char_buffer.clear();
147 } else {
148 return Err(StdUriTemplateError::new(format!(
149 "Failed to expand token, invalid at col:{}",
150 i
151 )));
152 }
153 }
154 ',' if to_token => {
155 let max_char = get_max_char(&max_char_buffer, i)?;
156 let expanded = expand_token(
157 operator.unwrap_or(Operator::NoOp),
158 &token,
159 composite,
160 max_char,
161 first_token,
162 substitutions,
163 &mut result,
164 i,
165 )?;
166 if expanded && first_token {
167 first_token = false;
168 }
169 token.clear();
170 composite = false;
171 to_max_char_buffer = false;
172 max_char_buffer.clear();
173 }
174 _ => {
175 if to_token {
176 if operator.is_none() {
177 operator = Some(get_operator(character, &mut token, i)?);
178 } else if to_max_char_buffer {
179 if character.is_ascii_digit() {
180 max_char_buffer.push(character);
181 } else {
182 return Err(StdUriTemplateError::new(format!(
183 "Illegal character identified in the token at col:{}",
184 i
185 )));
186 }
187 } else {
188 match character {
189 ':' => {
190 to_max_char_buffer = true;
191 max_char_buffer.clear();
192 }
193 '*' => {
194 composite = true;
195 }
196 _ => {
197 validate_literal(character, i)?;
198 token.push(character);
199 }
200 }
201 }
202 } else {
203 result.push(character);
204 }
205 }
206 }
207 }
208
209 if !to_token {
210 Ok(result)
211 } else {
212 Err(StdUriTemplateError::new("Unterminated token".to_string()))
213 }
214}
215
216fn add_prefix(op: Operator, result: &mut String) {
217 match op {
218 Operator::Hash => result.push('#'),
219 Operator::Dot => result.push('.'),
220 Operator::Slash => result.push('/'),
221 Operator::Semicolon => result.push(';'),
222 Operator::QuestionMark => result.push('?'),
223 Operator::Amp => result.push('&'),
224 _ => {}
225 }
226}
227
228fn add_separator(op: Operator, result: &mut String) {
229 match op {
230 Operator::Dot => result.push('.'),
231 Operator::Slash => result.push('/'),
232 Operator::Semicolon => result.push(';'),
233 Operator::QuestionMark | Operator::Amp => result.push('&'),
234 _ => result.push(','),
235 }
236}
237
238fn add_value(op: Operator, token: &str, value: &str, result: &mut String, max_char: i32) {
239 match op {
240 Operator::Plus | Operator::Hash => {
241 add_expanded_value(None, value, result, max_char, false);
242 }
243 Operator::QuestionMark | Operator::Amp => {
244 result.push_str(token);
245 result.push('=');
246 add_expanded_value(None, value, result, max_char, true);
247 }
248 Operator::Semicolon => {
249 result.push_str(token);
250 add_expanded_value(Some("="), value, result, max_char, true);
251 }
252 Operator::Dot | Operator::Slash | Operator::NoOp => {
253 add_expanded_value(None, value, result, max_char, true);
254 }
255 }
256}
257
258fn add_value_element(op: Operator, _token: &str, value: &str, result: &mut String, max_char: i32) {
259 match op {
260 Operator::Plus | Operator::Hash => {
261 add_expanded_value(None, value, result, max_char, false);
262 }
263 Operator::QuestionMark
264 | Operator::Amp
265 | Operator::Semicolon
266 | Operator::Dot
267 | Operator::Slash
268 | Operator::NoOp => {
269 add_expanded_value(None, value, result, max_char, true);
270 }
271 }
272}
273
274fn is_iprivate(cp: char) -> bool {
275 (0xE000..=0xF8FF).contains(&(cp as u32))
276}
277
278fn is_ucschar(cp: char) -> bool {
279 let code = cp as u32;
280 (0xA0..=0xD7FF).contains(&code)
281 || (0xF900..=0xFDCF).contains(&code)
282 || (0xFDF0..=0xFFEF).contains(&code)
283}
284
285fn is_unreserved(c: char) -> bool {
286 c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~'
287}
288
289fn percent_encode_char(c: char, result: &mut String) {
290 let mut buf = [0u8; 4];
291 let encoded = c.encode_utf8(&mut buf);
292 for byte in encoded.as_bytes() {
293 result.push('%');
294 result.push(to_hex_digit(byte >> 4));
295 result.push(to_hex_digit(byte & 0x0F));
296 }
297}
298
299fn url_encode_char(c: char, result: &mut String) {
300 if is_unreserved(c) {
301 result.push(c);
302 } else {
303 percent_encode_char(c, result);
304 }
305}
306
307fn to_hex_digit(nibble: u8) -> char {
308 match nibble {
309 0..=9 => (b'0' + nibble) as char,
310 10..=15 => (b'A' + nibble - 10) as char,
311 _ => unreachable!(),
312 }
313}
314
315fn add_expanded_value(
316 prefix: Option<&str>,
317 value: &str,
318 result: &mut String,
319 max_char: i32,
320 replace_reserved: bool,
321) {
322 let max = if max_char != -1 {
323 max_char as usize
324 } else {
325 usize::MAX
326 };
327
328 let mut to_reserved = false;
329 let mut reserved_buffer = String::with_capacity(3);
330 let mut to_append = String::with_capacity(12);
331 let mut prefix_pending = prefix;
332
333 for character in value.chars().take(max) {
334 if let Some(p) = prefix_pending.take() {
335 result.push_str(p);
336 }
337
338 if character == '%' && !replace_reserved {
339 to_reserved = true;
340 reserved_buffer.clear();
341 }
342
343 to_append.clear();
344 if replace_reserved || is_ucschar(character) || is_iprivate(character) {
345 url_encode_char(character, &mut to_append);
346 } else if !character.is_ascii() {
347 percent_encode_char(character, &mut to_append);
348 } else {
349 to_append.push(character);
350 }
351
352 if to_reserved {
353 reserved_buffer.push_str(&to_append);
354
355 if reserved_buffer.len() == 3 {
356 let is_encoded = is_valid_percent_encoded(&reserved_buffer);
357
358 if is_encoded {
359 result.push_str(&reserved_buffer);
360 } else {
361 result.push_str("%25");
362 result.push_str(&reserved_buffer[1..]);
363 }
364 to_reserved = false;
365 reserved_buffer.clear();
366 }
367 } else if character == ' ' {
368 result.push_str("%20");
369 } else if character == '%' {
370 result.push_str("%25");
371 } else {
372 result.push_str(&to_append);
373 }
374 }
375
376 if to_reserved {
377 result.push_str("%25");
378 result.push_str(&reserved_buffer[1..]);
379 }
380}
381
382fn is_valid_percent_encoded(s: &str) -> bool {
383 let b = s.as_bytes();
384 b.len() == 3 && b[0] == b'%' && b[1].is_ascii_hexdigit() && b[2].is_ascii_hexdigit()
385}
386
387fn get_substitution_type(
388 value: Option<&Value>,
389 _col: usize,
390) -> Result<SubstitutionType, StdUriTemplateError> {
391 match value {
392 None => Ok(SubstitutionType::Empty),
393 Some(v) => match v {
394 Value::String(_) | Value::Bool(_) | Value::Integer(_) | Value::Float(_) => {
395 Ok(SubstitutionType::String)
396 }
397 Value::List(_) => Ok(SubstitutionType::List),
398 Value::Map(_) => Ok(SubstitutionType::Map),
399 },
400 }
401}
402
403fn is_empty(subst_type: SubstitutionType, value: &Value) -> bool {
404 match subst_type {
405 SubstitutionType::String => false,
406 SubstitutionType::List => {
407 if let Value::List(l) = value {
408 l.is_empty()
409 } else {
410 true
411 }
412 }
413 SubstitutionType::Map => {
414 if let Value::Map(m) = value {
415 m.is_empty()
416 } else {
417 true
418 }
419 }
420 SubstitutionType::Empty => true,
421 }
422}
423
424fn convert_native_types(value: &Value) -> Result<Cow<'_, str>, StdUriTemplateError> {
425 match value {
426 Value::String(s) => Ok(Cow::Borrowed(s)),
427 Value::Bool(b) => Ok(Cow::Owned(b.to_string())),
428 Value::Integer(i) => Ok(Cow::Owned(i.to_string())),
429 Value::Float(f) => {
430 if *f == (*f as i64) as f64 && f.is_finite() {
431 Ok(Cow::Owned((*f as i64).to_string()))
432 } else {
433 Ok(Cow::Owned(f.to_string()))
434 }
435 }
436 Value::List(_) | Value::Map(_) => Err(StdUriTemplateError::new(format!(
437 "Illegal class passed as substitution, found {:?}",
438 value
439 ))),
440 }
441}
442
443#[allow(clippy::too_many_arguments)]
444fn expand_token(
445 operator: Operator,
446 token: &str,
447 composite: bool,
448 max_char: i32,
449 first_token: bool,
450 substitutions: &HashMap<String, Value>,
451 result: &mut String,
452 col: usize,
453) -> Result<bool, StdUriTemplateError> {
454 if token.is_empty() {
455 return Err(StdUriTemplateError::new(format!(
456 "Found an empty token at col:{}",
457 col
458 )));
459 }
460
461 let value = substitutions.get(token);
462 let subst_type = get_substitution_type(value, col)?;
463 if subst_type == SubstitutionType::Empty {
464 return Ok(false);
465 }
466
467 let value = value.unwrap();
468 if is_empty(subst_type, value) {
469 return Ok(false);
470 }
471
472 if first_token {
473 add_prefix(operator, result);
474 } else {
475 add_separator(operator, result);
476 }
477
478 match subst_type {
479 SubstitutionType::String => {
480 add_string_value(operator, token, value, result, max_char)?;
481 }
482 SubstitutionType::List => {
483 add_list_value(operator, token, value, result, max_char, composite)?;
484 }
485 SubstitutionType::Map => {
486 add_map_value(operator, token, value, result, max_char, composite)?;
487 }
488 SubstitutionType::Empty => {}
489 }
490
491 Ok(true)
492}
493
494fn add_string_value(
495 operator: Operator,
496 token: &str,
497 value: &Value,
498 result: &mut String,
499 max_char: i32,
500) -> Result<(), StdUriTemplateError> {
501 let s = convert_native_types(value)?;
502 add_value(operator, token, &s, result, max_char);
503 Ok(())
504}
505
506fn add_list_value(
507 operator: Operator,
508 token: &str,
509 value: &Value,
510 result: &mut String,
511 max_char: i32,
512 composite: bool,
513) -> Result<(), StdUriTemplateError> {
514 if let Value::List(list) = value {
515 let mut first = true;
516 for v in list {
517 let s = convert_native_types(v)?;
518 if first {
519 add_value(operator, token, &s, result, max_char);
520 first = false;
521 } else if composite {
522 add_separator(operator, result);
523 add_value(operator, token, &s, result, max_char);
524 } else {
525 result.push(',');
526 add_value_element(operator, token, &s, result, max_char);
527 }
528 }
529 }
530 Ok(())
531}
532
533fn add_map_value(
534 operator: Operator,
535 token: &str,
536 value: &Value,
537 result: &mut String,
538 max_char: i32,
539 composite: bool,
540) -> Result<(), StdUriTemplateError> {
541 if max_char != -1 {
542 return Err(StdUriTemplateError::new(
543 "Value trimming is not allowed on Maps".to_string(),
544 ));
545 }
546
547 if let Value::Map(map) = value {
548 let mut first = true;
549 for (key, val) in map {
550 let v = convert_native_types(val)?;
551 if composite {
552 if !first {
553 add_separator(operator, result);
554 }
555 add_value_element(operator, token, key, result, max_char);
556 result.push('=');
557 } else {
558 if first {
559 add_value(operator, token, key, result, max_char);
560 } else {
561 result.push(',');
562 add_value_element(operator, token, key, result, max_char);
563 }
564 result.push(',');
565 }
566 add_value_element(operator, token, &v, result, max_char);
567 first = false;
568 }
569 }
570
571 Ok(())
572}