fluent_uri/pct_enc/
estring.rs

1use super::{Assert, EStr, Encoder};
2use crate::{pct_enc::Encode, utf8::Utf8Chunks};
3use alloc::{borrow::ToOwned, string::String};
4use core::{borrow::Borrow, cmp::Ordering, fmt, hash, marker::PhantomData, ops::Deref};
5
6/// A percent-encoded, growable string.
7///
8/// The borrowed counterpart of `EString` is [`EStr`].
9/// See its documentation for the meaning of the type parameter `E`.
10///
11/// # Comparison
12///
13/// `EString`s are compared [lexicographically](Ord#lexicographical-comparison)
14/// by their byte values. Normalization is **not** performed prior to comparison.
15///
16/// # Examples
17///
18/// Encode key-value pairs to a query string and use it to build a URI reference:
19///
20/// ```
21/// use fluent_uri::{
22///     pct_enc::{
23///         encoder::{Data, Query},
24///         EStr, EString, Encoder, Table,
25///     },
26///     UriRef,
27/// };
28///
29/// let pairs = [("name", "张三"), ("speech", "¡Olé!")];
30/// let mut buf = EString::<Query>::new();
31/// for (k, v) in pairs {
32///     if !buf.is_empty() {
33///         buf.push('&');
34///     }
35///
36///     // WARNING: Absolutely do not confuse data with delimiters!
37///     // Use `Data` (or `IData`) to encode data contained in a URI
38///     // (or an IRI) unless you know what you're doing!
39///     buf.encode_str::<Data>(k);
40///     buf.push('=');
41///     buf.encode_str::<Data>(v);
42/// }
43///
44/// assert_eq!(buf, "name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21");
45///
46/// let uri_ref = UriRef::builder()
47///     .path(EStr::EMPTY)
48///     .query(&buf)
49///     .build()
50///     .unwrap();
51/// assert_eq!(uri_ref.as_str(), "?name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21");
52/// ```
53///
54/// Encode a path whose segments may contain the slash (`'/'`) character
55/// by using a custom sub-encoder:
56///
57/// ```
58/// use fluent_uri::pct_enc::{encoder::Path, EString, Encoder, Table};
59///
60/// struct PathSegment;
61///
62/// impl Encoder for PathSegment {
63///     const TABLE: &'static Table = &Path::TABLE.sub(&Table::new(b"/"));
64/// }
65///
66/// let mut path = EString::<Path>::new();
67/// path.push('/');
68/// path.encode_str::<PathSegment>("foo/bar");
69///
70/// assert_eq!(path, "/foo%2Fbar");
71/// ```
72#[derive(Clone, Default)]
73pub struct EString<E: Encoder> {
74    pub(crate) buf: String,
75    encoder: PhantomData<E>,
76}
77
78impl<E: Encoder> Deref for EString<E> {
79    type Target = EStr<E>;
80
81    fn deref(&self) -> &EStr<E> {
82        EStr::new_validated(&self.buf)
83    }
84}
85
86impl<E: Encoder> EString<E> {
87    pub(crate) fn new_validated(buf: String) -> Self {
88        Self {
89            buf,
90            encoder: PhantomData,
91        }
92    }
93
94    /// Creates a new empty `EString`.
95    #[must_use]
96    pub fn new() -> Self {
97        Self::new_validated(String::new())
98    }
99
100    /// Creates a new empty `EString` with at least the specified capacity.
101    #[must_use]
102    pub fn with_capacity(capacity: usize) -> Self {
103        Self::new_validated(String::with_capacity(capacity))
104    }
105
106    /// Coerces to an `EStr` slice.
107    #[must_use]
108    pub fn as_estr(&self) -> &EStr<E> {
109        self
110    }
111
112    /// Returns this `EString`'s capacity, in bytes.
113    #[must_use]
114    pub fn capacity(&self) -> usize {
115        self.buf.capacity()
116    }
117
118    /// Encodes a string with a sub-encoder and appends the result onto the end of this `EString`.
119    ///
120    /// A character will be preserved if `SubE::TABLE` [allows] it and percent-encoded otherwise.
121    ///
122    /// In most cases, use [`Data`] (for URI) or [`IData`] (for IRI) as the sub-encoder.
123    /// When using other sub-encoders, make sure that `SubE::TABLE` does not [allow][allows]
124    /// the component delimiters that delimit the data.
125    ///
126    /// Note that this method will **not** encode `U+0020` (space) as `U+002B` (+).
127    ///
128    /// [allows]: super::Table::allows
129    /// [`Data`]: super::encoder::Data
130    /// [`IData`]: super::encoder::IData
131    ///
132    /// # Panics
133    ///
134    /// Panics at compile time if `SubE` is not a [sub-encoder](Encoder#sub-encoders) of `E`,
135    /// or if `SubE::TABLE` does not [allow percent-encoded octets].
136    ///
137    /// [allow percent-encoded octets]: super::Table::allows_pct_encoded
138    pub fn encode_str<SubE: Encoder>(&mut self, s: &str) {
139        () = Assert::<SubE, E>::L_IS_SUB_ENCODER_OF_R;
140        () = EStr::<SubE>::ASSERT_ALLOWS_PCT_ENCODED;
141
142        for chunk in Encode::new(SubE::TABLE, s) {
143            self.buf.push_str(chunk.as_str());
144        }
145    }
146
147    /// Encodes a byte sequence with a sub-encoder and appends the result onto the end of this `EString`.
148    ///
149    /// A byte will be preserved if it is part of a UTF-8-encoded character
150    /// that `SubE::TABLE` [allows] and percent-encoded otherwise.
151    ///
152    /// In most cases, use [`Data`] (for URI) or [`IData`] (for IRI) as the sub-encoder.
153    /// When using other sub-encoders, make sure that `SubE::TABLE` does not [allow][allows]
154    /// the component delimiters that delimit the data.
155    ///
156    /// Note that this method will **not** encode `0x20` (space) as `U+002B` (+).
157    ///
158    /// If you need to encode a string, use [`encode_str`][Self::encode_str] instead.
159    ///
160    /// [allows]: super::Table::allows
161    /// [`Data`]: super::encoder::Data
162    /// [`IData`]: super::encoder::IData
163    ///
164    /// # Deprecation
165    ///
166    /// This method is deprecated because percent-encoding non-UTF-8 bytes is
167    /// a non-standard operation. If you're developing a new protocol, use
168    /// other encodings such as Base64 instead. If you absolutely must, here's
169    /// a workaround:
170    ///
171    /// ```
172    /// use fluent_uri::pct_enc::{encoder::Path, EStr, EString};
173    ///
174    /// let mut buf = EString::<Path>::new();
175    ///
176    /// for chunk in b"D\xFCrst".utf8_chunks() {
177    ///     buf.encode_str::<Path>(chunk.valid());
178    ///     for &x in chunk.invalid() {
179    ///         buf.push_estr(EStr::encode_byte(x));
180    ///     }
181    /// }
182    ///
183    /// assert_eq!(buf, "D%FCrst");
184    /// ```
185    ///
186    /// # Panics
187    ///
188    /// Panics at compile time if `SubE` is not a [sub-encoder](Encoder#sub-encoders) of `E`,
189    /// or if `SubE::TABLE` does not [allow percent-encoded octets].
190    ///
191    /// [allow percent-encoded octets]: super::Table::allows_pct_encoded
192    #[deprecated = "use `<[u8]>::utf8_chunks`, `EString::encode_str`, `EStr::encode_byte`, and `EString::push_estr` instead"]
193    pub fn encode_bytes<SubE: Encoder>(&mut self, bytes: &[u8]) {
194        () = Assert::<SubE, E>::L_IS_SUB_ENCODER_OF_R;
195        () = EStr::<SubE>::ASSERT_ALLOWS_PCT_ENCODED;
196
197        for chunk in Utf8Chunks::new(bytes) {
198            for chunk in Encode::new(SubE::TABLE, chunk.valid()) {
199                self.buf.push_str(chunk.as_str());
200            }
201            for &x in chunk.invalid() {
202                self.buf.push_str(super::encode_byte(x));
203            }
204        }
205    }
206
207    /// Appends an unencoded character onto the end of this `EString`.
208    ///
209    /// # Panics
210    ///
211    /// Panics if `E::TABLE` does not [allow] the character.
212    ///
213    /// [allow]: super::Table::allows
214    pub fn push(&mut self, ch: char) {
215        assert!(E::TABLE.allows(ch), "table does not allow the char");
216        self.buf.push(ch);
217    }
218
219    /// Appends an `EStr` slice onto the end of this `EString`.
220    pub fn push_estr(&mut self, s: &EStr<E>) {
221        self.buf.push_str(s.as_str());
222    }
223
224    /// Truncates this `EString`, removing all contents.
225    pub fn clear(&mut self) {
226        self.buf.clear();
227    }
228
229    /// Consumes this `EString` and yields the underlying `String`.
230    #[must_use]
231    pub fn into_string(self) -> String {
232        self.buf
233    }
234}
235
236impl<E: Encoder> AsRef<EStr<E>> for EString<E> {
237    fn as_ref(&self) -> &EStr<E> {
238        self
239    }
240}
241
242impl<E: Encoder> AsRef<str> for EString<E> {
243    fn as_ref(&self) -> &str {
244        &self.buf
245    }
246}
247
248impl<E: Encoder> Borrow<EStr<E>> for EString<E> {
249    fn borrow(&self) -> &EStr<E> {
250        self
251    }
252}
253
254impl<E: Encoder> From<&EStr<E>> for EString<E> {
255    fn from(s: &EStr<E>) -> Self {
256        s.to_owned()
257    }
258}
259
260impl<E: Encoder> PartialEq for EString<E> {
261    fn eq(&self, other: &Self) -> bool {
262        self.as_str() == other.as_str()
263    }
264}
265
266impl<E: Encoder> PartialEq<EStr<E>> for EString<E> {
267    fn eq(&self, other: &EStr<E>) -> bool {
268        self.as_str() == other.as_str()
269    }
270}
271
272impl<E: Encoder> PartialEq<EString<E>> for EStr<E> {
273    fn eq(&self, other: &EString<E>) -> bool {
274        self.as_str() == other.as_str()
275    }
276}
277
278impl<E: Encoder> PartialEq<&EStr<E>> for EString<E> {
279    fn eq(&self, other: &&EStr<E>) -> bool {
280        self.as_str() == other.as_str()
281    }
282}
283
284impl<E: Encoder> PartialEq<EString<E>> for &EStr<E> {
285    fn eq(&self, other: &EString<E>) -> bool {
286        self.as_str() == other.as_str()
287    }
288}
289
290impl<E: Encoder> PartialEq<str> for EString<E> {
291    fn eq(&self, other: &str) -> bool {
292        self.as_str() == other
293    }
294}
295
296impl<E: Encoder> PartialEq<EString<E>> for str {
297    fn eq(&self, other: &EString<E>) -> bool {
298        self == other.as_str()
299    }
300}
301
302impl<E: Encoder> PartialEq<&str> for EString<E> {
303    fn eq(&self, other: &&str) -> bool {
304        self.as_str() == *other
305    }
306}
307
308impl<E: Encoder> PartialEq<EString<E>> for &str {
309    fn eq(&self, other: &EString<E>) -> bool {
310        *self == other.as_str()
311    }
312}
313
314impl<E: Encoder> Eq for EString<E> {}
315
316impl<E: Encoder> hash::Hash for EString<E> {
317    fn hash<H: hash::Hasher>(&self, state: &mut H) {
318        self.buf.hash(state);
319    }
320}
321
322impl<E: Encoder> PartialOrd for EString<E> {
323    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
324        Some(self.cmp(other))
325    }
326}
327
328impl<E: Encoder> Ord for EString<E> {
329    fn cmp(&self, other: &Self) -> Ordering {
330        self.inner.cmp(&other.inner)
331    }
332}
333
334impl<E: Encoder> fmt::Debug for EString<E> {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        self.as_str().fmt(f)
337    }
338}
339
340impl<E: Encoder> fmt::Display for EString<E> {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        self.as_str().fmt(f)
343    }
344}