uri_template_core/template.rs
1use alloc::{borrow::Cow, string::String, vec::Vec};
2use core::ops::Range;
3
4use crate::__private::{Literal, Modifier, Operator, Piece, Variable};
5use crate::Values;
6use crate::utils::{decode_digit, decode_pct_hex, pct_encode_byte, truncate_str};
7
8#[derive(Debug)]
9pub struct UriTemplate {
10 pub(crate) raw: Cow<'static, str>,
11 pub(crate) pieces: Cow<'static, [Piece]>,
12}
13
14#[cfg(test)]
15impl UriTemplate {
16 pub fn variables(&self) -> Vec<&str> {
17 let mut names = Vec::new();
18 for piece in self.pieces.iter() {
19 if let Piece::Expression { variables, .. } = piece {
20 for var in variables.iter() {
21 names.push(&self.raw[var.range.clone()]);
22 }
23 }
24 }
25 names
26 }
27
28 pub fn literals(&self) -> Vec<&str> {
29 let mut literals = Vec::new();
30 for piece in self.pieces.iter() {
31 if let Piece::Literal(literal) = piece {
32 literals.push(self.raw_slice(literal.range()));
33 }
34 }
35 literals
36 }
37}
38
39impl UriTemplate {
40 pub fn new(input: impl Into<Cow<'static, str>>) -> Result<Self, Error> {
41 let raw = input.into();
42 let mut pieces = Cow::<[Piece]>::Owned(Vec::new());
43
44 let mut literal_start = 0usize;
45
46 let mut chars = raw.char_indices().peekable();
47 while let Some((byte_idx, ref ch)) = chars.next() {
48 match ch {
49 '{' => {
50 // avoid empty strings at the junction of two non-`Literal::Fine` pieces.
51 // such as:
52 // `%7B{var}`
53 // `你好{var}`
54 // `{var1}{var2}`
55 if literal_start < byte_idx {
56 pieces
57 .to_mut()
58 .push(Piece::Literal(Literal::Fine(literal_start..byte_idx)));
59 }
60 // parse expression
61 // parse operator
62 let Some((byte_idx, possible_operator)) = chars.peek() else {
63 return Err(Error::new_parse_error(
64 ParseError::UnmatchedBrace,
65 raw,
66 byte_idx,
67 ));
68 };
69 let mut out_byte_idx = *byte_idx;
70 let operator = match Operator::try_from_char(possible_operator) {
71 Some(Operator::Reserved(_)) => {
72 return Err(Error::new_parse_error(
73 ParseError::ReservedOperator,
74 raw,
75 out_byte_idx,
76 ));
77 }
78 op => op,
79 };
80
81 let mut var_name_start: usize = if operator.is_some() {
82 // has the operator, skip to next char
83 chars.next();
84 out_byte_idx + 1
85 } else {
86 // without the operator, continue with the current index
87 out_byte_idx
88 };
89 // parse variables
90 let mut variables = Cow::<[Variable]>::Owned(Vec::new());
91 let mut var_name_end = None;
92 let mut modifier: Option<Modifier> = None;
93 while let Some((inner_byte_idx, ref ch)) = chars.next() {
94 out_byte_idx = inner_byte_idx;
95 match ch {
96 '{' => {
97 return Err(Error::new_parse_error(
98 ParseError::NestedBrace,
99 raw,
100 inner_byte_idx,
101 ));
102 }
103 '}' => {
104 let var_name_end = var_name_end.unwrap_or(inner_byte_idx);
105 if var_name_start < var_name_end {
106 // example: `{*}`
107 variables.to_mut().push(Variable {
108 range: var_name_start..(var_name_end),
109 modifier: modifier.take(),
110 });
111 } else {
112 // example: `{}`
113 return Err(Error::new_parse_error(
114 ParseError::EmptyVariableName,
115 raw,
116 inner_byte_idx,
117 ));
118 }
119 break;
120 }
121 ',' => {
122 if var_name_start < inner_byte_idx {
123 variables.to_mut().push(Variable {
124 range: var_name_start
125 ..(var_name_end.unwrap_or(inner_byte_idx)),
126 modifier: modifier.take(),
127 });
128 // reset for next variable
129 var_name_start = inner_byte_idx + 1;
130 var_name_end = None;
131 modifier = None;
132 } else {
133 // example: `{a,}`
134 return Err(Error::new_parse_error(
135 ParseError::EmptyVariableName,
136 raw,
137 inner_byte_idx,
138 ));
139 }
140 }
141 '*' => {
142 // modifier: explode
143 var_name_end = Some(inner_byte_idx);
144 match modifier {
145 None => modifier = Some(Modifier::Explode),
146 _ => {
147 // example: `{var:3*}`
148 return Err(Error::new_parse_error(
149 ParseError::MultipleModifierInVariable,
150 raw,
151 inner_byte_idx,
152 ));
153 }
154 }
155 }
156 ':' => {
157 // modifier: prefix
158 var_name_end = Some(inner_byte_idx);
159 let mut acc = 0u16;
160
161 let mut digits_num = 0;
162 while let Some((byte_idx, ch)) = chars.peek()
163 && let Ok(digit) = decode_digit(ch)
164 {
165 digits_num += 1;
166 if digits_num == 0 && digit == 0 {
167 let byte_idx = *byte_idx;
168 return Err(Error::new_parse_error(
169 ParseError::PrefixLengthLeadingZero,
170 raw,
171 byte_idx,
172 ));
173 }
174
175 // RFC 6570
176 // max-length < 10000
177 if digits_num > 4 {
178 let byte_idx = *byte_idx;
179 return Err(Error::new_parse_error(
180 ParseError::PrefixLengthOverflow,
181 raw,
182 byte_idx,
183 ));
184 }
185 acc = acc * 10 + (digit as u16);
186 chars.next();
187 }
188 if acc == 0 {
189 return Err(Error::new_parse_error(
190 ParseError::PrefixLengthEmpty,
191 raw,
192 inner_byte_idx,
193 ));
194 }
195
196 match modifier {
197 None => modifier = Some(Modifier::Prefix(acc)),
198 _ => {
199 return Err(Error::new_parse_error(
200 ParseError::MultipleModifierInVariable,
201 raw,
202 inner_byte_idx,
203 ));
204 }
205 }
206 }
207 ch => {
208 // rfc: varname = varchar *( ["."] varchar )
209 // TODO: prohibit leading dots and consecutive dots in variable names
210 if is_varchar(ch) || ch == &'.' {
211 continue;
212 } else {
213 return Err(Error::new_parse_error(
214 ParseError::NotAllowedCharInExpression,
215 raw,
216 inner_byte_idx,
217 ));
218 }
219 }
220 }
221 }
222 if variables.is_empty() {
223 return Err(Error::new_parse_error(
224 ParseError::EmptyVariableName,
225 raw,
226 out_byte_idx,
227 ));
228 }
229 pieces.to_mut().push(Piece::Expression {
230 operator,
231 variables,
232 });
233 // reset literal buffer start
234 literal_start = out_byte_idx + 1;
235 }
236 '}' => {
237 return Err(Error::new_parse_error(
238 ParseError::UnmatchedBrace,
239 raw,
240 byte_idx,
241 ));
242 }
243 '%' => {
244 // avoid push redundant empty literals when using templates that start with '%'.
245 // for example: `%7Bvar%7D`
246 if literal_start < byte_idx {
247 pieces
248 .to_mut()
249 .push(Piece::Literal(Literal::Fine(literal_start..byte_idx)));
250 }
251 let mut pct_encoded_buf = Vec::new();
252 loop {
253 if let Some((_, c0)) = chars.next()
254 && let Some((__byte_idx, c1)) = chars.next()
255 && let Ok(byte) = decode_pct_hex(&c0, &c1)
256 {
257 pct_encoded_buf.push(byte);
258
259 match chars.peek() {
260 Some((_, '%')) => {
261 // continue to process the next percentage encoding
262 chars.next();
263 continue;
264 }
265 _ => {
266 // complete the percentage encoding processing
267 pieces.to_mut().push(Piece::Literal(Literal::Encoded {
268 range: byte_idx..__byte_idx + 1,
269 decoded: match String::from_utf8(pct_encoded_buf) {
270 Err(err) => Err(Cow::Owned(err.into_bytes())),
271 Ok(s) => Ok(Cow::Owned(s)),
272 },
273 }));
274 literal_start = __byte_idx + 1;
275 break;
276 }
277 }
278 } else {
279 return Err(Error::new_parse_error(
280 ParseError::NotAllowedSinglePercentInLiteral,
281 raw,
282 byte_idx,
283 ));
284 }
285 }
286 }
287 ch => match LiteralCharKind::from(ch) {
288 LiteralCharKind::Unreserved | LiteralCharKind::Reserved => {}
289 LiteralCharKind::NeedEncode => {
290 if literal_start < byte_idx {
291 pieces
292 .to_mut()
293 .push(Piece::Literal(Literal::Fine(literal_start..byte_idx)));
294 }
295 let mut literal_end = byte_idx + ch.len_utf8();
296 while let Some((byte_idx, ch)) = chars.peek()
297 && LiteralCharKind::from(ch) == LiteralCharKind::NeedEncode
298 {
299 literal_end = *byte_idx + ch.len_utf8();
300 chars.next();
301 }
302 pieces.to_mut().push(Piece::Literal(Literal::NeedEncode {
303 range: byte_idx..literal_end,
304 encoded: {
305 let to_encode = &raw[byte_idx..literal_end];
306 let mut encoded = String::with_capacity(to_encode.len() * 3);
307 for b in to_encode.as_bytes().iter() {
308 let [c0, c1, c2] = pct_encode_byte(b);
309 encoded.push(c0);
310 encoded.push(c1);
311 encoded.push(c2);
312 }
313 Cow::Owned(encoded)
314 },
315 }));
316 literal_start = literal_end;
317 }
318 LiteralCharKind::Invalid => {
319 return Err(Error::new_parse_error(
320 ParseError::NotAllowedCharInLiteral,
321 raw,
322 byte_idx,
323 ));
324 }
325 },
326 }
327 }
328
329 if literal_start < raw.len() {
330 pieces
331 .to_mut()
332 .push(Piece::Literal(Literal::Fine(literal_start..raw.len())));
333 }
334 Ok(Self { raw, pieces })
335 }
336}
337
338impl UriTemplate {
339 #[inline]
340 fn raw_slice(&self, range: &Range<usize>) -> &str {
341 &self.raw[range.clone()]
342 }
343
344 pub fn expand<'v, 's: 'v>(&self, values: impl Values<'v, 's>) -> Result<String, Error> {
345 let mut out = String::new();
346 for piece in self.pieces.iter() {
347 match piece {
348 Piece::Literal(literal) => match literal {
349 Literal::Fine(range) | Literal::Encoded { range, .. } => {
350 out.push_str(self.raw_slice(range));
351 }
352 Literal::NeedEncode { encoded, .. } => {
353 // https://www.rfc-editor.org/rfc/rfc6570#section-2.1
354 // ..., if not
355 // allowed, copied to the URI reference as the sequence of pct-encoded
356 // triplets corresponding to that character's encoding in UTF-8
357 out.push_str(encoded);
358 }
359 },
360 Piece::Expression {
361 operator,
362 variables,
363 } => {
364 let operator_mode = Operator::mode(operator);
365 let mut first_in_variables = true;
366
367 #[inline(always)]
368 fn push_str(out: &mut String, s: &str, should_encode_reserved: bool) {
369 if should_encode_reserved {
370 pct_encode_str_reserved(s, out);
371 } else {
372 pct_encode_str_unreserved(s, out);
373 }
374 }
375 for var in variables.iter() {
376 let var_name = self.raw_slice(var.range());
377 if let Some(value) = values.var(var_name)
378 && value.should_expand()
379 {
380 if first_in_variables {
381 // push leading char only if any of the variables are defined.
382 if let Some(leading) = operator_mode.leading {
383 out.push(leading);
384 }
385 first_in_variables = false;
386 } else {
387 out.push(operator_mode.separator);
388 }
389 match value.as_ref() {
390 crate::Value::Scalar(s) => {
391 if operator_mode.should_named {
392 out.push_str(var_name);
393 if !s.is_empty()
394 || operator_mode.should_append_equal_when_empty_string
395 {
396 out.push('=');
397 }
398 }
399 let s = if let Some(Modifier::Prefix(prefix)) = var.modifier {
400 truncate_str(s, prefix as usize)
401 } else {
402 s
403 };
404 push_str(&mut out, s, operator_mode.should_encode_reserved)
405 }
406 crate::Value::List(list) => match var.modifier {
407 None | Some(Modifier::Prefix(_)) => {
408 let mut first_in_list = true;
409 for s in list.iter() {
410 if first_in_list {
411 if operator_mode.should_named {
412 out.push_str(var_name);
413 out.push('=');
414 }
415 first_in_list = false;
416 } else {
417 out.push(',');
418 }
419 let s = if let Some(Modifier::Prefix(prefix)) =
420 var.modifier
421 {
422 truncate_str(s, prefix as usize)
423 } else {
424 s
425 };
426 push_str(
427 &mut out,
428 s,
429 operator_mode.should_encode_reserved,
430 );
431 }
432 }
433 Some(Modifier::Explode) => {
434 let mut first_in_list = true;
435 for s in list.iter() {
436 if first_in_list {
437 first_in_list = false;
438 } else {
439 out.push(operator_mode.separator);
440 }
441 if operator_mode.should_named {
442 out.push_str(var_name);
443 out.push('=');
444 }
445 push_str(
446 &mut out,
447 s,
448 operator_mode.should_encode_reserved,
449 );
450 }
451 }
452 },
453 crate::Value::Keys(keys) => match var.modifier {
454 Some(Modifier::Prefix(_)) => {
455 return Err(Error::new_expand_error(
456 "cannot apply prefix modifier to key-value pairs"
457 .into(),
458 ));
459 }
460 None => {
461 let mut first_in_keys = true;
462 for (key, val) in keys.iter() {
463 if first_in_keys {
464 if operator_mode.should_named {
465 out.push_str(var_name);
466 out.push('=');
467 }
468 first_in_keys = false;
469 } else {
470 out.push(',');
471 }
472 let s = if let Some(Modifier::Prefix(prefix)) =
473 var.modifier
474 {
475 truncate_str(key, prefix as usize)
476 } else {
477 key
478 };
479 push_str(
480 &mut out,
481 s,
482 operator_mode.should_encode_reserved,
483 );
484 out.push(',');
485 let s = if let Some(Modifier::Prefix(prefix)) =
486 var.modifier
487 {
488 truncate_str(val, prefix as usize)
489 } else {
490 val
491 };
492 push_str(
493 &mut out,
494 s,
495 operator_mode.should_encode_reserved,
496 );
497 }
498 }
499 Some(Modifier::Explode) => {
500 let mut first_in_keys = true;
501 for (key, val) in keys.iter() {
502 if first_in_keys {
503 first_in_keys = false;
504 } else {
505 out.push(operator_mode.separator);
506 }
507 push_str(
508 &mut out,
509 key,
510 operator_mode.should_encode_reserved,
511 );
512 out.push('=');
513 push_str(
514 &mut out,
515 val,
516 operator_mode.should_encode_reserved,
517 );
518 }
519 }
520 },
521 }
522 }
523 }
524 }
525 }
526 }
527 Ok(out)
528 }
529}
530
531impl Operator {
532 const fn try_from_char(ch: &char) -> Option<Self> {
533 match ch {
534 '+' => Some(Self::Plus),
535 '/' => Some(Self::Slash),
536 '.' => Some(Self::Dot),
537 ';' => Some(Self::Semi),
538 '?' => Some(Self::Question),
539 '&' => Some(Self::Ampersand),
540 '#' => Some(Self::Hash),
541 '=' | ',' | '!' | '@' | '|' => Some(Self::Reserved(*ch as u8)),
542 _ => None,
543 }
544 }
545
546 const fn mode(this: &Option<Self>) -> OperatorMode {
547 const DEFAULT: OperatorMode = OperatorMode {
548 leading: None,
549 separator: ',',
550 should_encode_reserved: true,
551 should_named: false,
552 should_append_equal_when_empty_string: false,
553 };
554 let Some(this) = this else {
555 return DEFAULT;
556 };
557 match this {
558 Self::Plus => OperatorMode {
559 leading: None,
560 separator: ',',
561 should_encode_reserved: false,
562 should_named: false,
563 should_append_equal_when_empty_string: false,
564 },
565 Self::Hash => OperatorMode {
566 separator: ',',
567 leading: Some('#'),
568 should_encode_reserved: false,
569 should_named: false,
570 should_append_equal_when_empty_string: false,
571 },
572 Self::Dot => OperatorMode {
573 leading: Some('.'),
574 separator: '.',
575 should_encode_reserved: true,
576 should_named: false,
577 should_append_equal_when_empty_string: false,
578 },
579 Self::Slash => OperatorMode {
580 leading: Some('/'),
581 separator: '/',
582 should_encode_reserved: true,
583 should_named: false,
584 should_append_equal_when_empty_string: false,
585 },
586 Self::Semi => OperatorMode {
587 leading: Some(';'),
588 separator: ';',
589 should_encode_reserved: true,
590 should_named: true,
591 should_append_equal_when_empty_string: false,
592 },
593 Self::Question => OperatorMode {
594 leading: Some('?'),
595 separator: '&',
596 should_encode_reserved: true,
597 should_named: true,
598 should_append_equal_when_empty_string: true,
599 },
600 Self::Ampersand => OperatorMode {
601 leading: Some('&'),
602 separator: '&',
603 should_encode_reserved: true,
604 should_named: true,
605 should_append_equal_when_empty_string: true,
606 },
607 Self::Reserved(_) => DEFAULT,
608 }
609 }
610}
611
612pub(crate) struct OperatorMode {
613 pub leading: Option<char>,
614 pub separator: char,
615 pub should_encode_reserved: bool,
616 pub should_named: bool,
617 pub should_append_equal_when_empty_string: bool,
618}
619
620#[derive(Debug)]
621pub struct Error {
622 kind: ErrorKind,
623}
624
625impl Error {
626 pub fn is_parse_error(&self) -> bool {
627 matches!(self.kind, ErrorKind::Parse { .. })
628 }
629
630 pub fn is_expand_error(&self) -> bool {
631 matches!(self.kind, ErrorKind::Expand(_))
632 }
633}
634
635#[derive(Debug)]
636enum ErrorKind {
637 Expand(Cow<'static, str>),
638 Parse {
639 kind: ParseError,
640 raw: Cow<'static, str>,
641 position: usize,
642 },
643}
644
645impl Error {
646 pub(crate) const fn new_expand_error(msg: Cow<'static, str>) -> Self {
647 Self {
648 kind: ErrorKind::Expand(msg),
649 }
650 }
651
652 pub(crate) const fn new_parse_error(
653 kind: ParseError,
654 raw: Cow<'static, str>,
655 position: usize,
656 ) -> Self {
657 Self {
658 kind: ErrorKind::Parse {
659 kind,
660 raw,
661 position,
662 },
663 }
664 }
665}
666
667#[derive(Debug)]
668pub enum ParseError {
669 /// `{var1{var2}}`
670 NestedBrace,
671 /// `{var`
672 UnmatchedBrace,
673 /// `{}`
674 EmptyVariableName,
675 ReservedOperator,
676 /// `{hello:2*}`
677 MultipleModifierInVariable,
678 // InvalidPercentEncoded,
679 // `%` only allowed in percent-encoding
680 NotAllowedSinglePercentInLiteral,
681 NotAllowedCharInLiteral,
682 NotAllowedCharInExpression,
683 /// `{var:}`
684 PrefixLengthEmpty,
685 /// `{var:03}`
686 PrefixLengthLeadingZero,
687 /// `{var:10000}`
688 PrefixLengthOverflow,
689}
690
691impl core::error::Error for Error {}
692impl core::fmt::Display for Error {
693 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
694 match &self.kind {
695 ErrorKind::Expand(msg) => write!(f, "expansion error: {}.", msg),
696 ErrorKind::Parse {
697 kind,
698 raw,
699 position,
700 } => {
701 write!(
702 f,
703 "parse error: {} at byte positions {}.\nraw: {}",
704 kind, position, raw
705 )
706 }
707 }
708 }
709}
710
711impl core::fmt::Display for ParseError {
712 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
713 match self {
714 Self::NestedBrace => write!(f, "nested brace '{{'"),
715 Self::UnmatchedBrace => write!(f, "unmatched `{{...` or `...}}`'"),
716 Self::EmptyVariableName => write!(f, "empty variable name `{{}}`"),
717 Self::ReservedOperator => write!(f, "not allowed reserved operator in expression",),
718 Self::NotAllowedSinglePercentInLiteral => write!(f, "not allowed `%` in literal",),
719 Self::NotAllowedCharInLiteral => write!(f, "not allowed char in literal"),
720 Self::NotAllowedCharInExpression => write!(f, "not allowed char in expression"),
721 Self::MultipleModifierInVariable => write!(f, "multiple modifier in variable"),
722 Self::PrefixLengthEmpty => write!(f, "empty prefix length"),
723 Self::PrefixLengthLeadingZero => write!(f, "leading zero prefix length"),
724 Self::PrefixLengthOverflow => write!(f, "prefix length overflows"),
725 }
726 }
727}
728
729#[derive(Debug, PartialEq, Eq)]
730enum LiteralCharKind {
731 Unreserved,
732 Reserved,
733 NeedEncode,
734 Invalid,
735}
736
737impl LiteralCharKind {
738 const fn from(ch: &char) -> Self {
739 match ch {
740 // ALPHA / DIGIT / "-" / "." / "_" / "~"
741 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '.' | '_' | '~' => {
742 LiteralCharKind::Unreserved
743 }
744 // gen-delims
745 ':' | '/' | '?' | '#' | '[' | ']' | '@' => LiteralCharKind::Reserved,
746 // sub-delims
747 // "'': https://www.rfc-editor.org/errata/eid6937
748 '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => {
749 LiteralCharKind::Reserved
750 }
751 // CTL
752 '\0'..='\x1F' | '\x7F' => LiteralCharKind::Invalid,
753 // other not allowed characters
754 // "%": reserved for percent-encoding
755 ' ' | '"' | '%' | '<' | '>' | '\\' | '^' | '`' | '{' | '|' | '}' => {
756 LiteralCharKind::Invalid
757 }
758 _ => LiteralCharKind::NeedEncode,
759 }
760 }
761}
762
763/// ALPHA / DIGIT / "_" / pct-encoded
764const fn is_varchar(ch: &char) -> bool {
765 matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '%')
766}
767
768const fn is_unreserved(c: char) -> bool {
769 matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '.' | '_' | '~')
770}
771
772const fn is_reserved(c: char) -> bool {
773 matches!(
774 c,
775 ':' | '/'
776 | '?'
777 | '#'
778 | '['
779 | ']'
780 | '@'
781 | '!'
782 | '$'
783 | '&'
784 | '\''
785 | '('
786 | ')'
787 | '*'
788 | '+'
789 | ','
790 | ';'
791 | '='
792 )
793}
794
795fn pct_encode_str_reserved(s: &str, out: &mut String) {
796 for ch in s.chars() {
797 if is_unreserved(ch) {
798 out.push(ch);
799 } else {
800 for b in ch.encode_utf8(&mut [0; 4]).as_bytes() {
801 let [c0, c1, c2] = pct_encode_byte(b);
802 out.push(c0);
803 out.push(c1);
804 out.push(c2);
805 }
806 }
807 }
808}
809
810/// used for `+` operator and `#` operator
811fn pct_encode_str_unreserved(s: &str, out: &mut String) {
812 let mut chars = s.char_indices();
813 while let Some((_, ch)) = chars.next() {
814 if ch == '%' {
815 let mut __chars = chars.clone();
816 if let Some((_, c0)) = __chars.next()
817 && let Some((_, c1)) = __chars.next()
818 && let Ok(_) = decode_pct_hex(&c0, &c1)
819 {
820 // handle percent-encoded sequences
821 out.push('%');
822 out.push(c0);
823 out.push(c1);
824 chars = __chars;
825 } else {
826 // invalid percent-encoding, encode '%' itself
827 out.push('%');
828 out.push('2');
829 out.push('5');
830 }
831 } else if is_reserved(ch) || is_unreserved(ch) {
832 out.push(ch);
833 } else {
834 for b in ch.encode_utf8(&mut [0; 4]).as_bytes() {
835 let [c0, c1, c2] = pct_encode_byte(b);
836 out.push(c0);
837 out.push(c1);
838 out.push(c2);
839 }
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn test_pct_encode_str_unreserved() {
850 let mut s = String::new();
851 pct_encode_str_reserved("!", &mut s);
852 assert_eq!(s, "%21");
853 }
854}