Coverage Report

Created: 2026-01-30 06:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/iri-string-0.7.9/src/percent_encode.rs
Line
Count
Source
1
//! Percent encoding.
2
3
use core::fmt::{self, Write as _};
4
use core::marker::PhantomData;
5
6
use crate::parser::char;
7
use crate::spec::{IriSpec, Spec, UriSpec};
8
9
/// A proxy to percent-encode a string as a part of URI.
10
pub type PercentEncodedForUri<T> = PercentEncoded<T, UriSpec>;
11
12
/// A proxy to percent-encode a string as a part of IRI.
13
pub type PercentEncodedForIri<T> = PercentEncoded<T, IriSpec>;
14
15
/// Context for percent encoding.
16
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17
#[non_exhaustive]
18
enum Context {
19
    /// Encode the string as a reg-name (usually called as "hostname").
20
    RegName,
21
    /// Encode the string as a user name or a password (inside the `userinfo` component).
22
    UserOrPassword,
23
    /// Encode the string as a path segment.
24
    ///
25
    /// A slash (`/`) will be encoded to `%2F`.
26
    PathSegment,
27
    /// Encode the string as path segments joined with `/`.
28
    ///
29
    /// A slash (`/`) will be used as is.
30
    Path,
31
    /// Encode the string as a query string (without the `?` prefix).
32
    Query,
33
    /// Encode the string as a fragment string (without the `#` prefix).
34
    Fragment,
35
    /// Encode all characters except for `unreserved` characters.
36
    Unreserve,
37
    /// Encode characters only if they cannot appear anywhere in an IRI reference.
38
    ///
39
    /// `%` character will be always encoded.
40
    Character,
41
}
42
43
/// A proxy to percent-encode a string.
44
///
45
/// Type aliases [`PercentEncodedForIri`] and [`PercentEncodedForUri`] are provided.
46
/// You can use them to make the expression simpler, for example write
47
/// `PercentEncodedForUri::from_path(foo)` instead of
48
/// `PercentEncoded::<_, UriSpec>::from_path(foo)`.
49
#[derive(Debug, Clone, Copy)]
50
pub struct PercentEncoded<T, S> {
51
    /// Source string context.
52
    context: Context,
53
    /// Raw string before being encoded.
54
    raw: T,
55
    /// Spec.
56
    _spec: PhantomData<fn() -> S>,
57
}
58
59
impl<T: fmt::Display, S: Spec> PercentEncoded<T, S> {
60
    /// Creates an encoded string from a raw reg-name (i.e. hostname or domain).
61
    ///
62
    /// # Examples
63
    ///
64
    /// ```
65
    /// # #[cfg(feature = "alloc")] {
66
    /// use iri_string::percent_encode::PercentEncoded;
67
    /// use iri_string::spec::UriSpec;
68
    ///
69
    /// let raw = "alpha.\u{03B1}.example.com";
70
    /// let encoded = "alpha.%CE%B1.example.com";
71
    /// assert_eq!(
72
    ///     PercentEncoded::<_, UriSpec>::from_reg_name(raw).to_string(),
73
    ///     encoded
74
    /// );
75
    /// # }
76
    /// ```
77
0
    pub fn from_reg_name(raw: T) -> Self {
78
0
        Self {
79
0
            context: Context::RegName,
80
0
            raw,
81
0
            _spec: PhantomData,
82
0
        }
83
0
    }
84
85
    /// Creates an encoded string from a raw user name (inside `userinfo` component).
86
    ///
87
    /// # Examples
88
    ///
89
    /// ```
90
    /// # #[cfg(feature = "alloc")] {
91
    /// use iri_string::percent_encode::PercentEncoded;
92
    /// use iri_string::spec::UriSpec;
93
    ///
94
    /// let raw = "user:\u{03B1}";
95
    /// // The first `:` will be interpreted as a delimiter, so colons will be escaped.
96
    /// let encoded = "user%3A%CE%B1";
97
    /// assert_eq!(
98
    ///     PercentEncoded::<_, UriSpec>::from_user(raw).to_string(),
99
    ///     encoded
100
    /// );
101
    /// # }
102
    /// ```
103
0
    pub fn from_user(raw: T) -> Self {
104
0
        Self {
105
0
            context: Context::UserOrPassword,
106
0
            raw,
107
0
            _spec: PhantomData,
108
0
        }
109
0
    }
110
111
    /// Creates an encoded string from a raw user name (inside `userinfo` component).
112
    ///
113
    /// # Examples
114
    ///
115
    /// ```
116
    /// # #[cfg(feature = "alloc")] {
117
    /// use iri_string::percent_encode::PercentEncoded;
118
    /// use iri_string::spec::UriSpec;
119
    ///
120
    /// let raw = "password:\u{03B1}";
121
    /// // The first `:` will be interpreted as a delimiter, and the colon
122
    /// // inside the password will be the first one if the user name is empty,
123
    /// // so colons will be escaped.
124
    /// let encoded = "password%3A%CE%B1";
125
    /// assert_eq!(
126
    ///     PercentEncoded::<_, UriSpec>::from_password(raw).to_string(),
127
    ///     encoded
128
    /// );
129
    /// # }
130
    /// ```
131
0
    pub fn from_password(raw: T) -> Self {
132
0
        Self {
133
0
            context: Context::UserOrPassword,
134
0
            raw,
135
0
            _spec: PhantomData,
136
0
        }
137
0
    }
138
139
    /// Creates an encoded string from a raw path segment.
140
    ///
141
    /// # Examples
142
    ///
143
    /// ```
144
    /// # #[cfg(feature = "alloc")] {
145
    /// use iri_string::percent_encode::PercentEncoded;
146
    /// use iri_string::spec::UriSpec;
147
    ///
148
    /// let raw = "alpha/\u{03B1}?#";
149
    /// // Note that `/` is encoded to `%2F`.
150
    /// let encoded = "alpha%2F%CE%B1%3F%23";
151
    /// assert_eq!(
152
    ///     PercentEncoded::<_, UriSpec>::from_path_segment(raw).to_string(),
153
    ///     encoded
154
    /// );
155
    /// # }
156
    /// ```
157
0
    pub fn from_path_segment(raw: T) -> Self {
158
0
        Self {
159
0
            context: Context::PathSegment,
160
0
            raw,
161
0
            _spec: PhantomData,
162
0
        }
163
0
    }
164
165
    /// Creates an encoded string from a raw path.
166
    ///
167
    /// # Examples
168
    ///
169
    /// ```
170
    /// # #[cfg(feature = "alloc")] {
171
    /// use iri_string::percent_encode::PercentEncoded;
172
    /// use iri_string::spec::UriSpec;
173
    ///
174
    /// let raw = "alpha/\u{03B1}?#";
175
    /// // Note that `/` is NOT percent encoded.
176
    /// let encoded = "alpha/%CE%B1%3F%23";
177
    /// assert_eq!(
178
    ///     PercentEncoded::<_, UriSpec>::from_path(raw).to_string(),
179
    ///     encoded
180
    /// );
181
    /// # }
182
    /// ```
183
0
    pub fn from_path(raw: T) -> Self {
184
0
        Self {
185
0
            context: Context::Path,
186
0
            raw,
187
0
            _spec: PhantomData,
188
0
        }
189
0
    }
190
191
    /// Creates an encoded string from a raw query.
192
    ///
193
    /// # Examples
194
    ///
195
    /// ```
196
    /// # #[cfg(feature = "alloc")] {
197
    /// use iri_string::percent_encode::PercentEncoded;
198
    /// use iri_string::spec::UriSpec;
199
    ///
200
    /// let raw = "alpha/\u{03B1}?#";
201
    /// let encoded = "alpha/%CE%B1?%23";
202
    /// assert_eq!(
203
    ///     PercentEncoded::<_, UriSpec>::from_query(raw).to_string(),
204
    ///     encoded
205
    /// );
206
    /// # }
207
    /// ```
208
0
    pub fn from_query(raw: T) -> Self {
209
0
        Self {
210
0
            context: Context::Query,
211
0
            raw,
212
0
            _spec: PhantomData,
213
0
        }
214
0
    }
215
216
    /// Creates an encoded string from a raw fragment.
217
    ///
218
    /// # Examples
219
    ///
220
    /// ```
221
    /// # #[cfg(feature = "alloc")] {
222
    /// use iri_string::percent_encode::PercentEncoded;
223
    /// use iri_string::spec::UriSpec;
224
    ///
225
    /// let raw = "alpha/\u{03B1}?#";
226
    /// let encoded = "alpha/%CE%B1?%23";
227
    /// assert_eq!(
228
    ///     PercentEncoded::<_, UriSpec>::from_fragment(raw).to_string(),
229
    ///     encoded
230
    /// );
231
    /// # }
232
    /// ```
233
0
    pub fn from_fragment(raw: T) -> Self {
234
0
        Self {
235
0
            context: Context::Fragment,
236
0
            raw,
237
0
            _spec: PhantomData,
238
0
        }
239
0
    }
240
241
    /// Creates a string consists of only `unreserved` string and percent-encoded triplets.
242
    ///
243
    /// # Examples
244
    ///
245
    /// ```
246
    /// # #[cfg(feature = "alloc")] {
247
    /// use iri_string::percent_encode::PercentEncoded;
248
    /// use iri_string::spec::UriSpec;
249
    ///
250
    /// let unreserved = "%a0-._~\u{03B1}";
251
    /// let unreserved_encoded = "%25a0-._~%CE%B1";
252
    /// assert_eq!(
253
    ///     PercentEncoded::<_, UriSpec>::unreserve(unreserved).to_string(),
254
    ///     unreserved_encoded
255
    /// );
256
    ///
257
    /// let reserved = ":/?#[]@ !$&'()*+,;=";
258
    /// let reserved_encoded =
259
    ///     "%3A%2F%3F%23%5B%5D%40%20%21%24%26%27%28%29%2A%2B%2C%3B%3D";
260
    /// assert_eq!(
261
    ///     PercentEncoded::<_, UriSpec>::unreserve(reserved).to_string(),
262
    ///     reserved_encoded
263
    /// );
264
    /// # }
265
    /// ```
266
    #[inline]
267
    #[must_use]
268
0
    pub fn unreserve(raw: T) -> Self {
269
0
        Self {
270
0
            context: Context::Unreserve,
271
0
            raw,
272
0
            _spec: PhantomData,
273
0
        }
274
0
    }
275
276
    /// Percent-encodes characters only if they cannot appear anywhere in an IRI reference.
277
    ///
278
    /// `%` character will be always encoded. In other words, this conversion
279
    /// is not aware of percent-encoded triplets.
280
    ///
281
    /// Note that this encoding process does not guarantee that the resulting
282
    /// string is a valid IRI reference.
283
    ///
284
    /// # Examples
285
    ///
286
    /// ```
287
    /// # #[cfg(feature = "alloc")] {
288
    /// use iri_string::percent_encode::PercentEncoded;
289
    /// use iri_string::spec::UriSpec;
290
    ///
291
    /// let unreserved = "%a0-._~\u{03B1}";
292
    /// let unreserved_encoded = "%25a0-._~%CE%B1";
293
    /// assert_eq!(
294
    ///     PercentEncoded::<_, UriSpec>::characters(unreserved).to_string(),
295
    ///     unreserved_encoded
296
    /// );
297
    ///
298
    /// let reserved = ":/?#[]@ !$&'()*+,;=";
299
    /// // Note that `%20` cannot appear directly in an IRI reference.
300
    /// let expected = ":/?#[]@%20!$&'()*+,;=";
301
    /// assert_eq!(
302
    ///     PercentEncoded::<_, UriSpec>::characters(reserved).to_string(),
303
    ///     expected
304
    /// );
305
    /// # }
306
    /// ```
307
    #[inline]
308
    #[must_use]
309
0
    pub fn characters(raw: T) -> Self {
310
0
        Self {
311
0
            context: Context::Character,
312
0
            raw,
313
0
            _spec: PhantomData,
314
0
        }
315
0
    }
316
}
317
318
impl<T: fmt::Display, S: Spec> fmt::Display for PercentEncoded<T, S> {
319
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320
        /// Filter that encodes a character before written if necessary.
321
        struct Filter<'a, 'b, S> {
322
            /// Encoding context.
323
            context: Context,
324
            /// Writer.
325
            writer: &'a mut fmt::Formatter<'b>,
326
            /// Spec.
327
            _spec: PhantomData<fn() -> S>,
328
        }
329
        impl<S: Spec> fmt::Write for Filter<'_, '_, S> {
330
0
            fn write_str(&mut self, s: &str) -> fmt::Result {
331
0
                s.chars().try_for_each(|c| self.write_char(c))
332
0
            }
333
0
            fn write_char(&mut self, c: char) -> fmt::Result {
334
0
                let is_valid_char = match (self.context, c.is_ascii()) {
335
0
                    (Context::RegName, true) => char::is_ascii_regname(c as u8),
336
0
                    (Context::RegName, false) => char::is_nonascii_regname::<S>(c),
337
                    (Context::UserOrPassword, true) => {
338
0
                        c != ':' && char::is_ascii_userinfo_ipvfutureaddr(c as u8)
339
                    }
340
0
                    (Context::UserOrPassword, false) => char::is_nonascii_userinfo::<S>(c),
341
0
                    (Context::PathSegment, true) => char::is_ascii_pchar(c as u8),
342
0
                    (Context::PathSegment, false) => S::is_nonascii_char_unreserved(c),
343
0
                    (Context::Path, true) => c == '/' || char::is_ascii_pchar(c as u8),
344
0
                    (Context::Path, false) => S::is_nonascii_char_unreserved(c),
345
0
                    (Context::Query, true) => c == '/' || char::is_ascii_frag_query(c as u8),
346
0
                    (Context::Query, false) => char::is_nonascii_query::<S>(c),
347
0
                    (Context::Fragment, true) => c == '/' || char::is_ascii_frag_query(c as u8),
348
0
                    (Context::Fragment, false) => char::is_nonascii_fragment::<S>(c),
349
0
                    (Context::Unreserve, true) => char::is_ascii_unreserved(c as u8),
350
0
                    (Context::Unreserve, false) => S::is_nonascii_char_unreserved(c),
351
0
                    (Context::Character, true) => char::is_ascii_unreserved_or_reserved(c as u8),
352
                    (Context::Character, false) => {
353
0
                        S::is_nonascii_char_unreserved(c) || S::is_nonascii_char_private(c)
354
                    }
355
                };
356
0
                if is_valid_char {
357
0
                    self.writer.write_char(c)
358
                } else {
359
0
                    write_pct_encoded_char(&mut self.writer, c)
360
                }
361
0
            }
362
        }
363
0
        let mut filter = Filter {
364
0
            context: self.context,
365
0
            writer: f,
366
0
            _spec: PhantomData::<fn() -> S>,
367
0
        };
368
0
        write!(filter, "{}", self.raw)
369
0
    }
370
}
371
372
/// Percent-encodes the given character and writes it.
373
#[inline]
374
0
fn write_pct_encoded_char<W: fmt::Write>(writer: &mut W, c: char) -> fmt::Result {
375
0
    let mut buf = [0_u8; 4];
376
0
    let buf = c.encode_utf8(&mut buf);
377
0
    buf.bytes().try_for_each(|b| write!(writer, "%{:02X}", b))
378
0
}