fluent_uri/pct_enc/mod.rs
1//! Percent-encoding utilities.
2
3pub mod encoder;
4#[cfg(feature = "alloc")]
5mod estring;
6pub(crate) mod table;
7
8#[cfg(feature = "alloc")]
9pub use estring::EString;
10pub use table::Table;
11
12use crate::imp::PathEncoder;
13use core::{cmp::Ordering, hash, iter::FusedIterator, marker::PhantomData, str};
14use ref_cast::{ref_cast_custom, RefCastCustom};
15
16#[cfg(feature = "alloc")]
17use alloc::{
18 borrow::{Cow, ToOwned},
19 string::String,
20 vec::Vec,
21};
22
23/// A trait used by [`EStr`] and [`EString`] to specify the table used for encoding.
24///
25/// # Sub-encoders
26///
27/// A sub-encoder `SubE` of `E` is an encoder such that `SubE::TABLE` is a [subset] of `E::TABLE`.
28///
29/// [subset]: Table::is_subset
30pub trait Encoder: 'static {
31 /// The table used for encoding.
32 const TABLE: &'static Table;
33}
34
35/// Percent-encoded string slices.
36///
37/// The owned counterpart of `EStr` is [`EString`]. See its documentation
38/// if you want to build a percent-encoded string from scratch.
39///
40/// # Type parameter
41///
42/// The `EStr<E>` type is parameterized over a type `E` that implements [`Encoder`].
43/// The associated constant `E::TABLE` of type [`Table`] specifies the byte patterns
44/// allowed in a string. In short, the underlying byte sequence of an `EStr<E>` slice
45/// can be formed by joining any number of the following byte sequences:
46///
47/// - `ch.encode_utf8(&mut [0; 4])` where `E::TABLE.allows(ch)`.
48/// - `[b'%', hi, lo]` where `E::TABLE.allows_pct_encoded() && hi.is_ascii_hexdigit() && lo.is_ascii_hexdigit()`.
49///
50/// # Comparison
51///
52/// `EStr` slices are compared [lexicographically](Ord#lexicographical-comparison)
53/// by their byte values. Normalization is **not** performed prior to comparison.
54///
55/// # Examples
56///
57/// Parse key-value pairs from a query string into a hash map:
58///
59/// ```
60/// use fluent_uri::{pct_enc::EStr, UriRef};
61/// use std::collections::HashMap;
62///
63/// let s = "?name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21";
64/// let query = UriRef::parse(s)?.query().unwrap();
65/// let map: HashMap<_, _> = query
66/// .split('&')
67/// .map(|s| s.split_once('=').unwrap_or((s, EStr::EMPTY)))
68/// .map(|(k, v)| (k.decode().to_string_lossy(), v.decode().to_string_lossy()))
69/// .collect();
70/// assert_eq!(map["name"], "张三");
71/// assert_eq!(map["speech"], "¡Olé!");
72/// # Ok::<_, fluent_uri::ParseError>(())
73/// ```
74#[derive(RefCastCustom)]
75#[repr(transparent)]
76pub struct EStr<E: Encoder> {
77 encoder: PhantomData<E>,
78 inner: str,
79}
80
81#[cfg(feature = "alloc")]
82struct Assert<L: Encoder, R: Encoder> {
83 _marker: PhantomData<(L, R)>,
84}
85
86#[cfg(feature = "alloc")]
87impl<L: Encoder, R: Encoder> Assert<L, R> {
88 const L_IS_SUB_ENCODER_OF_R: () = assert!(L::TABLE.is_subset(R::TABLE), "not a sub-encoder");
89}
90
91impl<E: Encoder> EStr<E> {
92 const ASSERT_ALLOWS_PCT_ENCODED: () = assert!(
93 E::TABLE.allows_pct_encoded(),
94 "table does not allow percent-encoded octets"
95 );
96
97 /// Converts a string slice to an `EStr` slice assuming validity.
98 #[ref_cast_custom]
99 pub(crate) const fn new_validated(s: &str) -> &Self;
100
101 /// An empty `EStr` slice.
102 pub const EMPTY: &'static Self = Self::new_validated("");
103
104 pub(crate) fn cast<F: Encoder>(&self) -> &EStr<F> {
105 EStr::new_validated(&self.inner)
106 }
107
108 /// Converts a string slice to an `EStr` slice.
109 ///
110 /// # Panics
111 ///
112 /// Panics if the string is not properly encoded with `E`.
113 /// For a non-panicking variant, use [`new`](Self::new).
114 #[must_use]
115 pub const fn new_or_panic(s: &str) -> &Self {
116 match Self::new(s) {
117 Some(s) => s,
118 None => panic!("improperly encoded string"),
119 }
120 }
121
122 /// Converts a string slice to an `EStr` slice, returning `None` if the conversion fails.
123 #[must_use]
124 pub const fn new(s: &str) -> Option<&Self> {
125 if E::TABLE.validate(s.as_bytes()) {
126 Some(Self::new_validated(s))
127 } else {
128 None
129 }
130 }
131
132 /// Yields the underlying string slice.
133 #[must_use]
134 pub fn as_str(&self) -> &str {
135 &self.inner
136 }
137
138 /// Returns the length of the `EStr` slice in bytes.
139 #[must_use]
140 pub fn len(&self) -> usize {
141 self.inner.len()
142 }
143
144 /// Checks whether the `EStr` slice is empty.
145 #[must_use]
146 pub fn is_empty(&self) -> bool {
147 self.inner.is_empty()
148 }
149
150 /// Upcasts the `EStr` slice to associate it with the given super-encoder.
151 ///
152 /// # Panics
153 ///
154 /// Panics at compile time if `E` is not a [sub-encoder](Encoder#sub-encoders) of `SuperE`.
155 ///
156 /// # Example
157 ///
158 /// ```
159 /// use fluent_uri::pct_enc::{encoder::{IPath, Path}, EStr};
160 ///
161 /// let path = EStr::<Path>::new_or_panic("foo");
162 /// let path: &EStr<IPath> = path.upcast();
163 /// ```
164 #[cfg(fluent_uri_unstable)]
165 #[must_use]
166 pub fn upcast<SuperE: Encoder>(&self) -> &EStr<SuperE> {
167 () = Assert::<E, SuperE>::L_IS_SUB_ENCODER_OF_R;
168 EStr::new_validated(self.as_str())
169 }
170
171 /// Checks whether the `EStr` slice is unencoded, i.e., does not contain `'%'`.
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use fluent_uri::pct_enc::{encoder::Path, EStr};
177 ///
178 /// assert!(EStr::<Path>::new_or_panic("Hello!").is_unencoded());
179 /// assert!(!EStr::<Path>::new_or_panic("%C2%A1Hola%21").is_unencoded());
180 /// ```
181 #[cfg(fluent_uri_unstable)]
182 #[must_use]
183 pub fn is_unencoded(&self) -> bool {
184 !(E::TABLE.allows_pct_encoded() && self.inner.contains('%'))
185 }
186
187 /// Returns an iterator used to decode the `EStr` slice.
188 ///
189 /// Always **split before decoding**, as otherwise the data may be
190 /// mistaken for component delimiters.
191 ///
192 /// Note that the iterator will **not** decode `U+002B` (+) as `0x20` (space).
193 ///
194 /// # Panics
195 ///
196 /// Panics at compile time if `E::TABLE` does not [allow percent-encoded octets].
197 ///
198 /// [allow percent-encoded octets]: Table::allows_pct_encoded
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use fluent_uri::pct_enc::{encoder::Path, EStr};
204 ///
205 /// let dec = EStr::<Path>::new_or_panic("%C2%A1Hola%21").decode();
206 /// assert_eq!(*dec.clone().to_bytes(), [0xc2, 0xa1, 0x48, 0x6f, 0x6c, 0x61, 0x21]);
207 /// assert_eq!(dec.to_string().unwrap(), "¡Hola!");
208 /// ```
209 pub fn decode(&self) -> Decode<'_> {
210 () = Self::ASSERT_ALLOWS_PCT_ENCODED;
211 Decode {
212 source: &self.inner,
213 }
214 }
215
216 /// Returns an iterator over subslices of the `EStr` slice separated by the given delimiter.
217 ///
218 /// # Panics
219 ///
220 /// Panics if the delimiter is not a [reserved] character.
221 ///
222 /// [reserved]: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// use fluent_uri::pct_enc::{encoder::Path, EStr};
228 ///
229 /// assert!(EStr::<Path>::new_or_panic("a,b,c").split(',').eq(["a", "b", "c"]));
230 /// assert!(EStr::<Path>::new_or_panic(",").split(',').eq(["", ""]));
231 /// assert!(EStr::<Path>::EMPTY.split(',').eq([""]));
232 /// ```
233 pub fn split(&self, delim: char) -> Split<'_, E> {
234 assert!(
235 delim.is_ascii() && table::RESERVED.allows(delim),
236 "splitting with non-reserved character"
237 );
238 Split {
239 inner: self.inner.split(delim),
240 encoder: PhantomData,
241 }
242 }
243
244 /// Splits the `EStr` slice on the first occurrence of the given delimiter and
245 /// returns prefix before delimiter and suffix after delimiter.
246 ///
247 /// Returns `None` if the delimiter is not found.
248 ///
249 /// # Panics
250 ///
251 /// Panics if the delimiter is not a [reserved] character.
252 ///
253 /// [reserved]: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// use fluent_uri::pct_enc::{encoder::Path, EStr};
259 ///
260 /// assert_eq!(
261 /// EStr::<Path>::new_or_panic("foo;bar;baz").split_once(';'),
262 /// Some((EStr::new_or_panic("foo"), EStr::new_or_panic("bar;baz")))
263 /// );
264 ///
265 /// assert_eq!(EStr::<Path>::new_or_panic("foo").split_once(';'), None);
266 /// ```
267 #[must_use]
268 pub fn split_once(&self, delim: char) -> Option<(&Self, &Self)> {
269 assert!(
270 delim.is_ascii() && table::RESERVED.allows(delim),
271 "splitting with non-reserved character"
272 );
273 self.inner
274 .split_once(delim)
275 .map(|(a, b)| (Self::new_validated(a), Self::new_validated(b)))
276 }
277
278 /// Splits the `EStr` slice on the last occurrence of the given delimiter and
279 /// returns prefix before delimiter and suffix after delimiter.
280 ///
281 /// Returns `None` if the delimiter is not found.
282 ///
283 /// # Panics
284 ///
285 /// Panics if the delimiter is not a [reserved] character.
286 ///
287 /// [reserved]: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// use fluent_uri::pct_enc::{encoder::Path, EStr};
293 ///
294 /// assert_eq!(
295 /// EStr::<Path>::new_or_panic("foo;bar;baz").rsplit_once(';'),
296 /// Some((EStr::new_or_panic("foo;bar"), EStr::new_or_panic("baz")))
297 /// );
298 ///
299 /// assert_eq!(EStr::<Path>::new_or_panic("foo").rsplit_once(';'), None);
300 /// ```
301 #[must_use]
302 pub fn rsplit_once(&self, delim: char) -> Option<(&Self, &Self)> {
303 assert!(
304 delim.is_ascii() && table::RESERVED.allows(delim),
305 "splitting with non-reserved character"
306 );
307 self.inner
308 .rsplit_once(delim)
309 .map(|(a, b)| (Self::new_validated(a), Self::new_validated(b)))
310 }
311}
312
313impl<E: Encoder> AsRef<Self> for EStr<E> {
314 fn as_ref(&self) -> &Self {
315 self
316 }
317}
318
319impl<E: Encoder> AsRef<str> for EStr<E> {
320 fn as_ref(&self) -> &str {
321 &self.inner
322 }
323}
324
325impl<E: Encoder> PartialEq for EStr<E> {
326 fn eq(&self, other: &Self) -> bool {
327 self.inner == other.inner
328 }
329}
330
331impl<E: Encoder> PartialEq<str> for EStr<E> {
332 fn eq(&self, other: &str) -> bool {
333 &self.inner == other
334 }
335}
336
337impl<E: Encoder> PartialEq<EStr<E>> for str {
338 fn eq(&self, other: &EStr<E>) -> bool {
339 self == &other.inner
340 }
341}
342
343impl<E: Encoder> Eq for EStr<E> {}
344
345impl<E: Encoder> hash::Hash for EStr<E> {
346 fn hash<H: hash::Hasher>(&self, state: &mut H) {
347 self.inner.hash(state);
348 }
349}
350
351impl<E: Encoder> PartialOrd for EStr<E> {
352 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
353 Some(self.cmp(other))
354 }
355}
356
357impl<E: Encoder> Ord for EStr<E> {
358 fn cmp(&self, other: &Self) -> Ordering {
359 self.inner.cmp(&other.inner)
360 }
361}
362
363impl<E: Encoder> Default for &EStr<E> {
364 /// Creates an empty `EStr` slice.
365 fn default() -> Self {
366 EStr::EMPTY
367 }
368}
369
370#[cfg(feature = "alloc")]
371impl<E: Encoder> ToOwned for EStr<E> {
372 type Owned = EString<E>;
373
374 fn to_owned(&self) -> EString<E> {
375 EString::new_validated(self.inner.to_owned())
376 }
377
378 fn clone_into(&self, target: &mut EString<E>) {
379 self.inner.clone_into(&mut target.buf);
380 }
381}
382
383/// Extension methods for the [path] component.
384///
385/// [path]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
386impl<E: PathEncoder> EStr<E> {
387 /// Checks whether the path is absolute, i.e., starting with `'/'`.
388 #[inline]
389 #[must_use]
390 pub fn is_absolute(&self) -> bool {
391 self.inner.starts_with('/')
392 }
393
394 /// Checks whether the path is rootless, i.e., not starting with `'/'`.
395 #[inline]
396 #[must_use]
397 pub fn is_rootless(&self) -> bool {
398 !self.inner.starts_with('/')
399 }
400
401 /// Returns an iterator over the path segments, separated by `'/'`.
402 ///
403 /// Returns `None` if the path is [rootless]. Use [`split`]
404 /// instead if you need to split a rootless path on occurrences of `'/'`.
405 ///
406 /// Note that the path can be [empty] when authority is present,
407 /// in which case this method will return `None`.
408 ///
409 /// [rootless]: Self::is_rootless
410 /// [`split`]: Self::split
411 /// [empty]: Self::is_empty
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// use fluent_uri::Uri;
417 ///
418 /// // Segments are separated by '/'.
419 /// // The empty string before a leading '/' is not a segment.
420 /// // However, segments can be empty in the other cases.
421 /// let path = Uri::parse("file:///path/to//dir/")?.path();
422 /// assert_eq!(path, "/path/to//dir/");
423 /// assert!(path.segments_if_absolute().unwrap().eq(["path", "to", "", "dir", ""]));
424 ///
425 /// let path = Uri::parse("foo:bar/baz")?.path();
426 /// assert_eq!(path, "bar/baz");
427 /// assert!(path.segments_if_absolute().is_none());
428 ///
429 /// let path = Uri::parse("http://example.com")?.path();
430 /// assert!(path.is_empty());
431 /// assert!(path.segments_if_absolute().is_none());
432 /// # Ok::<_, fluent_uri::ParseError>(())
433 /// ```
434 #[inline]
435 #[must_use]
436 pub fn segments_if_absolute(&self) -> Option<Split<'_, E>> {
437 self.inner
438 .strip_prefix('/')
439 .map(|s| Self::new_validated(s).split('/'))
440 }
441}
442
443const fn gen_octet_table(hi: bool) -> [u8; 256] {
444 let mut out = [0xff; 256];
445 let shift = if hi { 4 } else { 0 };
446
447 let mut i = 0;
448 while i < 10 {
449 out[(i + b'0') as usize] = i << shift;
450 i += 1;
451 }
452 while i < 16 {
453 out[(i - 10 + b'A') as usize] = i << shift;
454 out[(i - 10 + b'a') as usize] = i << shift;
455 i += 1;
456 }
457 out
458}
459
460const OCTET_TABLE_HI: &[u8; 256] = &gen_octet_table(true);
461pub(crate) const OCTET_TABLE_LO: &[u8; 256] = &gen_octet_table(false);
462
463/// Decodes a percent-encoded octet, assuming that the bytes are hexadecimal.
464pub(crate) fn decode_octet(hi: u8, lo: u8) -> u8 {
465 debug_assert!(hi.is_ascii_hexdigit() && lo.is_ascii_hexdigit());
466 OCTET_TABLE_HI[hi as usize] | OCTET_TABLE_LO[lo as usize]
467}
468
469#[cfg(feature = "alloc")]
470pub(crate) fn encode_byte(x: u8, buf: &mut alloc::string::String) {
471 const HEX_TABLE: [u8; 512] = {
472 const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
473
474 let mut i = 0;
475 let mut table = [0; 512];
476 while i < 256 {
477 table[i * 2] = HEX_DIGITS[i >> 4];
478 table[i * 2 + 1] = HEX_DIGITS[i & 0b1111];
479 i += 1;
480 }
481 table
482 };
483
484 buf.push('%');
485 buf.push(HEX_TABLE[x as usize * 2] as char);
486 buf.push(HEX_TABLE[x as usize * 2 + 1] as char);
487}
488
489/// An iterator used to decode an [`EStr`] slice.
490///
491/// This struct is created by [`EStr::decode`]. Normally you'll use the methods below
492/// instead of iterating over a `Decode` manually, unless you need precise control
493/// over allocation.
494///
495/// See the [`DecodedChunk`] type for documentation of the items yielded by this iterator.
496#[derive(Clone, Debug)]
497#[must_use = "iterators are lazy and do nothing unless consumed"]
498pub struct Decode<'a> {
499 source: &'a str,
500}
501
502/// An item returned by the [`Decode`] iterator.
503#[derive(Clone, Copy, Debug)]
504pub enum DecodedChunk<'a> {
505 /// An unencoded subslice.
506 Unencoded(&'a str),
507 /// A percent-encoded octet, decoded (for example, `"%20"` decoded as `0x20`).
508 PctDecoded(u8),
509}
510
511impl<'a> Iterator for Decode<'a> {
512 type Item = DecodedChunk<'a>;
513
514 fn next(&mut self) -> Option<Self::Item> {
515 if self.source.is_empty() {
516 return None;
517 }
518
519 let i = self
520 .source
521 .bytes()
522 .position(|x| x == b'%')
523 .unwrap_or(self.source.len());
524
525 if i == 0 {
526 let (s, rest) = self.source.split_at(3);
527 let x = decode_octet(s.as_bytes()[1], s.as_bytes()[2]);
528 self.source = rest;
529 Some(DecodedChunk::PctDecoded(x))
530 } else {
531 let (s, rest) = self.source.split_at(i);
532 self.source = rest;
533 Some(DecodedChunk::Unencoded(s))
534 }
535 }
536}
537
538impl FusedIterator for Decode<'_> {}
539
540#[cfg(feature = "alloc")]
541enum DecodedUtf8Chunk<'a, 'b> {
542 Unencoded(&'a str),
543 Decoded { valid: &'b str, invalid: &'b [u8] },
544}
545
546#[cfg(feature = "alloc")]
547fn decode_utf8<'a>(
548 iter: impl Iterator<Item = DecodedChunk<'a>>,
549 mut handle_chunk: impl FnMut(DecodedUtf8Chunk<'a, '_>),
550) {
551 use crate::utf8::Utf8Chunks;
552
553 let mut buf = [0; 32];
554 let mut cnt = 0;
555
556 'decode: for chunk in iter {
557 match chunk {
558 DecodedChunk::Unencoded(s) => {
559 if cnt > 0 {
560 for chunk in Utf8Chunks::new(&buf[..cnt]) {
561 handle_chunk(DecodedUtf8Chunk::Decoded {
562 valid: chunk.valid(),
563 invalid: chunk.invalid(),
564 });
565 }
566 cnt = 0;
567 }
568 handle_chunk(DecodedUtf8Chunk::Unencoded(s));
569 }
570 DecodedChunk::PctDecoded(x) => {
571 buf[cnt] = x;
572 cnt += 1;
573
574 if cnt >= buf.len() {
575 for chunk in Utf8Chunks::new(&buf[..cnt]) {
576 if chunk.incomplete() {
577 handle_chunk(DecodedUtf8Chunk::Decoded {
578 valid: chunk.valid(),
579 invalid: &[],
580 });
581
582 let invalid_len = chunk.invalid().len();
583 buf.copy_within(cnt - invalid_len..cnt, 0);
584
585 cnt = invalid_len;
586 continue 'decode;
587 }
588 handle_chunk(DecodedUtf8Chunk::Decoded {
589 valid: chunk.valid(),
590 invalid: chunk.invalid(),
591 });
592 }
593 cnt = 0;
594 }
595 }
596 }
597 }
598
599 for chunk in Utf8Chunks::new(&buf[..cnt]) {
600 handle_chunk(DecodedUtf8Chunk::Decoded {
601 valid: chunk.valid(),
602 invalid: chunk.invalid(),
603 });
604 }
605}
606
607#[cfg(feature = "alloc")]
608impl<'a> Decode<'a> {
609 /// Decodes the slice to bytes.
610 ///
611 /// This method allocates only when the slice contains any percent-encoded octet.
612 #[must_use]
613 pub fn to_bytes(self) -> Cow<'a, [u8]> {
614 let len = self.source.len();
615 let mut iter = self.peekable();
616
617 let mut buf;
618 match iter.peek() {
619 Some(&DecodedChunk::Unencoded(s)) => {
620 iter.next();
621 if iter.peek().is_none() {
622 return Cow::Borrowed(s.as_bytes());
623 }
624 buf = Vec::with_capacity(len);
625 buf.extend_from_slice(s.as_bytes());
626 }
627 None => return Cow::Borrowed(&[]),
628 _ => buf = Vec::with_capacity(len),
629 }
630
631 for chunk in iter {
632 match chunk {
633 DecodedChunk::Unencoded(s) => buf.extend_from_slice(s.as_bytes()),
634 DecodedChunk::PctDecoded(s) => buf.push(s),
635 }
636 }
637 Cow::Owned(buf)
638 }
639
640 /// Attempts to decode the slice to a string.
641 ///
642 /// This method allocates only when the slice contains any percent-encoded octet.
643 ///
644 /// # Errors
645 ///
646 /// Returns `Err` containing the decoded bytes if they are not valid UTF-8.
647 pub fn to_string(self) -> Result<Cow<'a, str>, Vec<u8>> {
648 let len = self.source.len();
649 let mut iter = self.peekable();
650
651 let mut buf;
652 match iter.peek() {
653 Some(&DecodedChunk::Unencoded(s)) => {
654 iter.next();
655 if iter.peek().is_none() {
656 return Ok(Cow::Borrowed(s));
657 }
658 buf = String::with_capacity(len);
659 buf.push_str(s);
660 }
661 None => return Ok(Cow::Borrowed("")),
662 _ => buf = String::with_capacity(len),
663 }
664
665 let mut buf = Ok::<_, Vec<u8>>(buf);
666
667 decode_utf8(iter, |chunk| match chunk {
668 DecodedUtf8Chunk::Unencoded(s) => match &mut buf {
669 Ok(string) => string.push_str(s),
670 Err(vec) => vec.extend_from_slice(s.as_bytes()),
671 },
672 DecodedUtf8Chunk::Decoded { valid, invalid } => match &mut buf {
673 Ok(string) => {
674 string.push_str(valid);
675 if !invalid.is_empty() {
676 let mut vec = core::mem::take(string).into_bytes();
677 vec.extend_from_slice(invalid);
678 buf = Err(vec);
679 }
680 }
681 Err(vec) => {
682 vec.extend_from_slice(valid.as_bytes());
683 vec.extend_from_slice(invalid);
684 }
685 },
686 });
687
688 match buf {
689 Ok(buf) => Ok(Cow::Owned(buf)),
690 Err(buf) => Err(buf),
691 }
692 }
693
694 /// Decodes the slice to a string, replacing any invalid UTF-8 sequences with
695 /// [`U+FFFD REPLACEMENT CHARACTER`][U+FFFD].
696 ///
697 /// [U+FFFD]: core::char::REPLACEMENT_CHARACTER
698 ///
699 /// This method allocates only when the slice contains any percent-encoded octet.
700 #[must_use]
701 pub fn to_string_lossy(self) -> Cow<'a, str> {
702 let len = self.source.len();
703 let mut iter = self.peekable();
704
705 let mut buf;
706 match iter.peek() {
707 Some(&DecodedChunk::Unencoded(s)) => {
708 iter.next();
709 if iter.peek().is_none() {
710 return Cow::Borrowed(s);
711 }
712 buf = String::with_capacity(len);
713 buf.push_str(s);
714 }
715 None => return Cow::Borrowed(""),
716 _ => buf = String::with_capacity(len),
717 }
718
719 decode_utf8(iter, |chunk| match chunk {
720 DecodedUtf8Chunk::Unencoded(s) => buf.push_str(s),
721 DecodedUtf8Chunk::Decoded { valid, invalid } => {
722 buf.push_str(valid);
723 if !invalid.is_empty() {
724 buf.push(char::REPLACEMENT_CHARACTER);
725 }
726 }
727 });
728 Cow::Owned(buf)
729 }
730}
731
732/// An iterator over subslices of an [`EStr`] slice separated by a delimiter.
733///
734/// This struct is created by [`EStr::split`].
735#[derive(Clone, Debug)]
736#[must_use = "iterators are lazy and do nothing unless consumed"]
737pub struct Split<'a, E: Encoder> {
738 inner: str::Split<'a, char>,
739 encoder: PhantomData<E>,
740}
741
742impl<'a, E: Encoder> Iterator for Split<'a, E> {
743 type Item = &'a EStr<E>;
744
745 fn next(&mut self) -> Option<&'a EStr<E>> {
746 self.inner.next().map(EStr::new_validated)
747 }
748}
749
750impl<'a, E: Encoder> DoubleEndedIterator for Split<'a, E> {
751 fn next_back(&mut self) -> Option<&'a EStr<E>> {
752 self.inner.next_back().map(EStr::new_validated)
753 }
754}
755
756impl<E: Encoder> FusedIterator for Split<'_, E> {}