Просмотр исходного кода

Percent-encoding yields `&str` instead of `char`.

Simon Sapin 10 лет назад
Родитель
Сommit
8813343598
1 измененных файлов с 89 добавлено и 39 удалено
  1. 89 39
      src/percent_encoding.rs

+ 89 - 39
src/percent_encoding.rs

@@ -10,6 +10,7 @@ use std::ascii::AsciiExt;
 use std::borrow::Cow;
 use std::fmt::{self, Write};
 use std::slice;
+use std::str;
 
 /// Represents a set of characters / bytes that should be percent-encoded.
 ///
@@ -26,7 +27,7 @@ use std::slice;
 /// Use the [`define_encode_set!`](../macro.define_encode_set!.html) macro to define different ones.
 pub trait EncodeSet: Clone {
     /// Called with UTF-8 bytes rather than code points.
-    /// Should return false for all non-ASCII bytes.
+    /// Should return true for all non-ASCII bytes.
     fn contains(&self, byte: u8) -> bool;
 }
 
@@ -116,82 +117,131 @@ define_encode_set! {
     }
 }
 
-/// Percent-encode the given bytes and return an iterator of `char` in the ASCII range.
+/// Return the percent-encoding of the given bytes.
+///
+/// This is unconditional, unlike `percent_encode()` which uses an encode set.
+pub fn percent_encode_byte(byte: u8) -> &'static str {
+    let index = usize::from(byte) * 3;
+    &"\
+        %00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F\
+        %10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F\
+        %20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F\
+        %30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F\
+        %40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F\
+        %50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F\
+        %60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F\
+        %70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F\
+        %80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F\
+        %90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F\
+        %A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF\
+        %B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF\
+        %C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF\
+        %D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF\
+        %E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF\
+        %F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF\
+    "[index..index + 3]
+}
+
+/// Percent-encode the given bytes with the given encode set.
+///
+/// The encode set define which bytes (in addition to non-ASCII and controls)
+/// need to be percent-encoded.
+/// The choice of this set depends on context.
+/// For example, `?` needs to be encoded in an URL path but not in a query string.
+///
+/// The return value is an iterator of `&str` slices (so it has a `.collect::<String>()` method)
+/// that also implements `Display` and `Into<Cow<str>>`.
+/// The latter returns `Cow::Borrowed` when none of the bytes in `input`
+/// are in the given encode set.
 #[inline]
 pub fn percent_encode<E: EncodeSet>(input: &[u8], encode_set: E) -> PercentEncode<E> {
     PercentEncode {
-        iter: input.iter(),
+        bytes: input,
         encode_set: encode_set,
-        state: PercentEncodeState::NextByte,
     }
 }
 
-/// Percent-encode the UTF-8 encoding of the given string
-/// and return an iterator of `char` in the ASCII range.
+/// Percent-encode the UTF-8 encoding of the given string.
+///
+/// See `percent_decode()` for how to use the return value.
 #[inline]
 pub fn utf8_percent_encode<E: EncodeSet>(input: &str, encode_set: E) -> PercentEncode<E> {
     percent_encode(input.as_bytes(), encode_set)
 }
 
+/// The return type of `percent_decode()`.
 #[derive(Clone)]
 pub struct PercentEncode<'a, E: EncodeSet> {
-    iter: slice::Iter<'a, u8>,
+    bytes: &'a [u8],
     encode_set: E,
-    state: PercentEncodeState,
-}
-
-#[derive(Clone)]
-enum PercentEncodeState {
-    NextByte,
-    HexHigh(u8),
-    HexLow(u8),
 }
 
 impl<'a, E: EncodeSet> Iterator for PercentEncode<'a, E> {
-    type Item = char;
-
-    fn next(&mut self) -> Option<char> {
-        // str::char::from_digit always returns lowercase.
-        const UPPER_HEX: [char; 16] = ['0', '1', '2', '3', '4', '5', '6', '7',
-                                       '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
-        match self.state {
-            PercentEncodeState::HexHigh(byte) => {
-                self.state = PercentEncodeState::HexLow(byte);
-                Some(UPPER_HEX[(byte >> 4) as usize])
-            }
-            PercentEncodeState::HexLow(byte) => {
-                self.state = PercentEncodeState::NextByte;
-                Some(UPPER_HEX[(byte & 0x0F) as usize])
-            }
-            PercentEncodeState::NextByte => {
-                self.iter.next().map(|&byte| {
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<&'a str> {
+        if let Some((&first_byte, remaining)) = self.bytes.split_first() {
+            if self.encode_set.contains(first_byte) {
+                self.bytes = remaining;
+                Some(percent_encode_byte(first_byte))
+            } else {
+                assert!(first_byte.is_ascii());
+                for (i, &byte) in remaining.iter().enumerate() {
                     if self.encode_set.contains(byte) {
-                        self.state = PercentEncodeState::HexHigh(byte);
-                        '%'
+                        // 1 for first_byte + i for previous iterations of this loop
+                        let (unchanged_slice, remaining) = self.bytes.split_at(1 + i);
+                        self.bytes = remaining;
+                        return Some(unsafe { str::from_utf8_unchecked(unchanged_slice) })
                     } else {
                         assert!(byte.is_ascii());
-                        byte as char
                     }
-                })
+                }
+                let unchanged_slice = self.bytes;
+                self.bytes = &[][..];
+                Some(unsafe { str::from_utf8_unchecked(unchanged_slice) })
             }
+        } else {
+            None
         }
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
-        let (low, high) = self.iter.size_hint();
-        (low.saturating_add(2) / 3, high)
+        if self.bytes.is_empty() {
+            (0, Some(0))
+        } else {
+            (1, Some(self.bytes.len()))
+        }
     }
 }
 
 impl<'a, E: EncodeSet> fmt::Display for PercentEncode<'a, E> {
     fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
         for c in (*self).clone() {
-            try!(formatter.write_char(c))
+            try!(formatter.write_str(c))
         }
         Ok(())
     }
 }
 
+impl<'a, E: EncodeSet> From<PercentEncode<'a, E>> for Cow<'a, str> {
+    fn from(mut iter: PercentEncode<'a, E>) -> Self {
+        match iter.next() {
+            None => "".into(),
+            Some(first) => {
+                match iter.next() {
+                    None => first.into(),
+                    Some(second) => {
+                        let mut string = first.to_owned();
+                        string.push_str(second);
+                        string.extend(iter);
+                        string.into()
+                    }
+                }
+            }
+        }
+    }
+}
+
 /// Percent-decode the given bytes and return an iterator of bytes.
 #[inline]
 pub fn percent_decode(input: &[u8]) -> PercentDecode {