1 | use std::convert::Infallible; |
2 | use std::fmt; |
3 | use std::fmt::Write; |
4 | |
5 | use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode}; |
6 | |
7 | use crate::filters::{FastWritable, HtmlSafeOutput}; |
8 | |
9 | // Urlencode char encoding set. Only the characters in the unreserved set don't |
10 | // have any special purpose in any part of a URI and can be safely left |
11 | // unencoded as specified in https://tools.ietf.org/html/rfc3986.html#section-2.3 |
12 | const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC |
13 | .remove(b'_' ) |
14 | .remove(b'.' ) |
15 | .remove(b'-' ) |
16 | .remove(byte:b'~' ); |
17 | |
18 | // Same as URLENCODE_STRICT_SET, but preserves forward slashes for encoding paths |
19 | const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(byte:b'/' ); |
20 | |
21 | /// Percent-encodes the argument for safe use in URI; does not encode `/`. |
22 | /// |
23 | /// This should be safe for all parts of URI (paths segments, query keys, query |
24 | /// values). In the rare case that the server can't deal with forward slashes in |
25 | /// the query string, use [`urlencode_strict`], which encodes them as well. |
26 | /// |
27 | /// Encodes all characters except ASCII letters, digits, and `_.-~/`. In other |
28 | /// words, encodes all characters which are not in the unreserved set, |
29 | /// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3), |
30 | /// with the exception of `/`. |
31 | /// |
32 | /// ```none,ignore |
33 | /// <a href="/metro{{ "/stations/Château d'Eau"|urlencode }}">Station</a> |
34 | /// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode }}">Page</a> |
35 | /// ``` |
36 | /// |
37 | /// To encode `/` as well, see [`urlencode_strict`](./fn.urlencode_strict.html). |
38 | /// |
39 | /// [`urlencode_strict`]: ./fn.urlencode_strict.html |
40 | /// |
41 | /// ``` |
42 | /// # #[cfg (feature = "code-in-doc" )] { |
43 | /// # use rinja::Template; |
44 | /// /// ```jinja |
45 | /// /// <div>{{ example|urlencode }}</div> |
46 | /// /// ``` |
47 | /// #[derive(Template)] |
48 | /// #[template(ext = "html" , in_doc = true)] |
49 | /// struct Example<'a> { |
50 | /// example: &'a str, |
51 | /// } |
52 | /// |
53 | /// assert_eq!( |
54 | /// Example { example: "hello?world" }.to_string(), |
55 | /// "<div>hello%3Fworld</div>" |
56 | /// ); |
57 | /// # } |
58 | /// ``` |
59 | #[inline ] |
60 | pub fn urlencode<T>(s: T) -> Result<HtmlSafeOutput<UrlencodeFilter<T>>, Infallible> { |
61 | Ok(HtmlSafeOutput(UrlencodeFilter(s, URLENCODE_SET))) |
62 | } |
63 | |
64 | /// Percent-encodes the argument for safe use in URI; encodes `/`. |
65 | /// |
66 | /// Use this filter for encoding query keys and values in the rare case that |
67 | /// the server can't process them unencoded. |
68 | /// |
69 | /// Encodes all characters except ASCII letters, digits, and `_.-~`. In other |
70 | /// words, encodes all characters which are not in the unreserved set, |
71 | /// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3). |
72 | /// |
73 | /// ```none,ignore |
74 | /// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode_strict }}">Page</a> |
75 | /// ``` |
76 | /// |
77 | /// If you want to preserve `/`, see [`urlencode`](./fn.urlencode.html). |
78 | /// |
79 | /// ``` |
80 | /// # #[cfg (feature = "code-in-doc" )] { |
81 | /// # use rinja::Template; |
82 | /// /// ```jinja |
83 | /// /// <a href='{{ example|urlencode_strict }}'>Example</a> |
84 | /// /// ``` |
85 | /// #[derive(Template)] |
86 | /// #[template(ext = "html" , in_doc = true)] |
87 | /// struct Example<'a> { |
88 | /// example: &'a str, |
89 | /// } |
90 | /// |
91 | /// assert_eq!( |
92 | /// Example { example: "/hello/world" }.to_string(), |
93 | /// "<a href='%2Fhello%2Fworld'>Example</a>" |
94 | /// ); |
95 | /// # } |
96 | /// ``` |
97 | #[inline ] |
98 | pub fn urlencode_strict<T>(s: T) -> Result<HtmlSafeOutput<UrlencodeFilter<T>>, Infallible> { |
99 | Ok(HtmlSafeOutput(UrlencodeFilter(s, URLENCODE_STRICT_SET))) |
100 | } |
101 | |
102 | pub struct UrlencodeFilter<T>(pub T, pub &'static AsciiSet); |
103 | |
104 | impl<T: fmt::Display> fmt::Display for UrlencodeFilter<T> { |
105 | #[inline ] |
106 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
107 | write!(UrlencodeWriter(f, self.1), " {}" , self.0) |
108 | } |
109 | } |
110 | |
111 | impl<T: FastWritable> FastWritable for UrlencodeFilter<T> { |
112 | #[inline ] |
113 | fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> fmt::Result { |
114 | self.0.write_into(&mut UrlencodeWriter(f, self.1)) |
115 | } |
116 | } |
117 | |
118 | struct UrlencodeWriter<W>(W, &'static AsciiSet); |
119 | |
120 | impl<W: fmt::Write> fmt::Write for UrlencodeWriter<W> { |
121 | #[inline ] |
122 | fn write_str(&mut self, s: &str) -> fmt::Result { |
123 | write!(self.0, " {}" , utf8_percent_encode(s, self.1)) |
124 | } |
125 | } |
126 | |
127 | #[test ] |
128 | fn test_urlencoding() { |
129 | // Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3) |
130 | // alpha / digit |
131 | assert_eq!(urlencode("AZaz09" ).unwrap().to_string(), "AZaz09" ); |
132 | assert_eq!(urlencode_strict("AZaz09" ).unwrap().to_string(), "AZaz09" ); |
133 | // other |
134 | assert_eq!(urlencode("_.-~" ).unwrap().to_string(), "_.-~" ); |
135 | assert_eq!(urlencode_strict("_.-~" ).unwrap().to_string(), "_.-~" ); |
136 | |
137 | // Reserved (https://tools.ietf.org/html/rfc3986.html#section-2.2) |
138 | // gen-delims |
139 | assert_eq!( |
140 | urlencode(":/?#[]@" ).unwrap().to_string(), |
141 | "%3A/%3F%23%5B%5D%40" |
142 | ); |
143 | assert_eq!( |
144 | urlencode_strict(":/?#[]@" ).unwrap().to_string(), |
145 | "%3A%2F%3F%23%5B%5D%40" |
146 | ); |
147 | // sub-delims |
148 | assert_eq!( |
149 | urlencode("!$&'()*+,;=" ).unwrap().to_string(), |
150 | "%21%24%26%27%28%29%2A%2B%2C%3B%3D" |
151 | ); |
152 | assert_eq!( |
153 | urlencode_strict("!$&'()*+,;=" ).unwrap().to_string(), |
154 | "%21%24%26%27%28%29%2A%2B%2C%3B%3D" |
155 | ); |
156 | |
157 | // Other |
158 | assert_eq!( |
159 | urlencode("žŠďŤňĚáÉóŮ" ).unwrap().to_string(), |
160 | "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE" |
161 | ); |
162 | assert_eq!( |
163 | urlencode_strict("žŠďŤňĚáÉóŮ" ).unwrap().to_string(), |
164 | "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE" |
165 | ); |
166 | |
167 | // Ferris |
168 | assert_eq!(urlencode("🦀" ).unwrap().to_string(), "%F0%9F%A6%80" ); |
169 | assert_eq!(urlencode_strict("🦀" ).unwrap().to_string(), "%F0%9F%A6%80" ); |
170 | } |
171 | |