Skip to main content

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}