lib.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. //! Processing of `data:` URLs according to the Fetch Standard:
  2. //! <https://fetch.spec.whatwg.org/#data-urls>
  3. //! but starting from a string rather than a parsed URL to avoid extra copies.
  4. //!
  5. //! ```rust
  6. //! use data_url::{DataUrl, mime};
  7. //!
  8. //! let url = DataUrl::process("data:,Hello%20World!").unwrap();
  9. //! let (body, fragment) = url.decode_to_vec().unwrap();
  10. //!
  11. //! assert_eq!(url.mime_type().type_, "text");
  12. //! assert_eq!(url.mime_type().subtype, "plain");
  13. //! assert_eq!(url.mime_type().get_parameter("charset"), Some("US-ASCII"));
  14. //! assert_eq!(body, b"Hello World!");
  15. //! assert!(fragment.is_none());
  16. //! ```
  17. #![no_std]
  18. // For forwards compatibility
  19. #[cfg(feature = "std")]
  20. extern crate std;
  21. #[macro_use]
  22. extern crate alloc;
  23. #[cfg(not(feature = "alloc"))]
  24. compile_error!("the `alloc` feature must be enabled");
  25. use alloc::{string::String, vec::Vec};
  26. use core::fmt;
  27. macro_rules! require {
  28. ($condition: expr) => {
  29. if !$condition {
  30. return None;
  31. }
  32. };
  33. }
  34. pub mod forgiving_base64;
  35. pub mod mime;
  36. pub struct DataUrl<'a> {
  37. mime_type: mime::Mime,
  38. base64: bool,
  39. encoded_body_plus_fragment: &'a str,
  40. }
  41. #[derive(Debug)]
  42. pub enum DataUrlError {
  43. NotADataUrl,
  44. NoComma,
  45. }
  46. impl fmt::Display for DataUrlError {
  47. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  48. match self {
  49. Self::NotADataUrl => write!(f, "not a valid data url"),
  50. Self::NoComma => write!(
  51. f,
  52. "data url is missing comma delimiting attributes and body"
  53. ),
  54. }
  55. }
  56. }
  57. #[cfg(feature = "std")]
  58. impl std::error::Error for DataUrlError {}
  59. impl<'a> DataUrl<'a> {
  60. /// <https://fetch.spec.whatwg.org/#data-url-processor>
  61. /// but starting from a string rather than a parsed `Url`, to avoid extra string copies.
  62. pub fn process(input: &'a str) -> Result<Self, DataUrlError> {
  63. use crate::DataUrlError::*;
  64. let after_colon = pretend_parse_data_url(input).ok_or(NotADataUrl)?;
  65. let (from_colon_to_comma, encoded_body_plus_fragment) =
  66. find_comma_before_fragment(after_colon).ok_or(NoComma)?;
  67. let (mime_type, base64) = parse_header(from_colon_to_comma);
  68. Ok(DataUrl {
  69. mime_type,
  70. base64,
  71. encoded_body_plus_fragment,
  72. })
  73. }
  74. pub fn mime_type(&self) -> &mime::Mime {
  75. &self.mime_type
  76. }
  77. /// Streaming-decode the data URL’s body to `write_body_bytes`,
  78. /// and return the URL’s fragment identifier if it has one.
  79. pub fn decode<F, E>(
  80. &self,
  81. write_body_bytes: F,
  82. ) -> Result<Option<FragmentIdentifier<'a>>, forgiving_base64::DecodeError<E>>
  83. where
  84. F: FnMut(&[u8]) -> Result<(), E>,
  85. {
  86. if self.base64 {
  87. decode_with_base64(self.encoded_body_plus_fragment, write_body_bytes)
  88. } else {
  89. decode_without_base64(self.encoded_body_plus_fragment, write_body_bytes)
  90. .map_err(forgiving_base64::DecodeError::WriteError)
  91. }
  92. }
  93. /// Return the decoded body, and the URL’s fragment identifier if it has one.
  94. pub fn decode_to_vec(
  95. &self,
  96. ) -> Result<(Vec<u8>, Option<FragmentIdentifier<'a>>), forgiving_base64::InvalidBase64> {
  97. let mut body = Vec::new();
  98. let fragment = self.decode(|bytes| {
  99. body.extend_from_slice(bytes);
  100. Ok(())
  101. })?;
  102. Ok((body, fragment))
  103. }
  104. }
  105. /// The URL’s fragment identifier (after `#`)
  106. pub struct FragmentIdentifier<'a>(&'a str);
  107. impl FragmentIdentifier<'_> {
  108. /// Like in a parsed URL
  109. pub fn to_percent_encoded(&self) -> String {
  110. let mut string = String::new();
  111. for byte in self.0.bytes() {
  112. match byte {
  113. // Ignore ASCII tabs or newlines like the URL parser would
  114. b'\t' | b'\n' | b'\r' => continue,
  115. // https://url.spec.whatwg.org/#fragment-percent-encode-set
  116. b'\0'..=b' ' | b'"' | b'<' | b'>' | b'`' | b'\x7F'..=b'\xFF' => {
  117. percent_encode(byte, &mut string)
  118. }
  119. // Printable ASCII
  120. _ => string.push(byte as char),
  121. }
  122. }
  123. string
  124. }
  125. }
  126. /// Similar to <https://url.spec.whatwg.org/#concept-basic-url-parser>
  127. /// followed by <https://url.spec.whatwg.org/#concept-url-serializer>
  128. ///
  129. /// * `None`: not a data URL.
  130. ///
  131. /// * `Some(s)`: sort of the result of serialization, except:
  132. ///
  133. /// - `data:` prefix removed
  134. /// - The fragment is included
  135. /// - Other components are **not** UTF-8 percent-encoded
  136. /// - ASCII tabs and newlines in the middle are **not** removed
  137. fn pretend_parse_data_url(input: &str) -> Option<&str> {
  138. // Trim C0 control or space
  139. let left_trimmed = input.trim_start_matches(|ch| ch <= ' ');
  140. let mut bytes = left_trimmed.bytes();
  141. {
  142. // Ignore ASCII tabs or newlines like the URL parser would
  143. let mut iter = bytes
  144. .by_ref()
  145. .filter(|&byte| !matches!(byte, b'\t' | b'\n' | b'\r'));
  146. require!(iter.next()?.eq_ignore_ascii_case(&b'd'));
  147. require!(iter.next()?.eq_ignore_ascii_case(&b'a'));
  148. require!(iter.next()?.eq_ignore_ascii_case(&b't'));
  149. require!(iter.next()?.eq_ignore_ascii_case(&b'a'));
  150. require!(iter.next()? == b':');
  151. }
  152. let bytes_consumed = left_trimmed.len() - bytes.len();
  153. let after_colon = &left_trimmed[bytes_consumed..];
  154. // Trim C0 control or space
  155. Some(after_colon.trim_end_matches(|ch| ch <= ' '))
  156. }
  157. fn find_comma_before_fragment(after_colon: &str) -> Option<(&str, &str)> {
  158. for (i, byte) in after_colon.bytes().enumerate() {
  159. if byte == b',' {
  160. return Some((&after_colon[..i], &after_colon[i + 1..]));
  161. }
  162. if byte == b'#' {
  163. break;
  164. }
  165. }
  166. None
  167. }
  168. fn parse_header(from_colon_to_comma: &str) -> (mime::Mime, bool) {
  169. // "Strip leading and trailing ASCII whitespace"
  170. // \t, \n, and \r would have been filtered by the URL parser
  171. // \f percent-encoded by the URL parser
  172. // space is the only remaining ASCII whitespace
  173. let trimmed = from_colon_to_comma.trim_matches(|c| matches!(c, ' ' | '\t' | '\n' | '\r'));
  174. let without_base64_suffix = remove_base64_suffix(trimmed);
  175. let base64 = without_base64_suffix.is_some();
  176. let mime_type = without_base64_suffix.unwrap_or(trimmed);
  177. let mut string = String::new();
  178. if mime_type.starts_with(';') {
  179. string.push_str("text/plain")
  180. }
  181. let mut in_query = false;
  182. for byte in mime_type.bytes() {
  183. match byte {
  184. // Ignore ASCII tabs or newlines like the URL parser would
  185. b'\t' | b'\n' | b'\r' => continue,
  186. // https://url.spec.whatwg.org/#c0-control-percent-encode-set
  187. b'\0'..=b'\x1F' | b'\x7F'..=b'\xFF' => percent_encode(byte, &mut string),
  188. // Bytes other than the C0 percent-encode set that are percent-encoded
  189. // by the URL parser in the query state.
  190. // '#' is also in that list but cannot occur here
  191. // since it indicates the start of the URL’s fragment.
  192. b' ' | b'"' | b'<' | b'>' if in_query => percent_encode(byte, &mut string),
  193. b'?' => {
  194. in_query = true;
  195. string.push('?')
  196. }
  197. // Printable ASCII
  198. _ => string.push(byte as char),
  199. }
  200. }
  201. // FIXME: does Mime::from_str match the MIME Sniffing Standard’s parsing algorithm?
  202. // <https://mimesniff.spec.whatwg.org/#parse-a-mime-type>
  203. let mime_type = string.parse().unwrap_or_else(|_| mime::Mime {
  204. type_: String::from("text"),
  205. subtype: String::from("plain"),
  206. parameters: vec![(String::from("charset"), String::from("US-ASCII"))],
  207. });
  208. (mime_type, base64)
  209. }
  210. /// None: no base64 suffix
  211. #[allow(clippy::skip_while_next)]
  212. fn remove_base64_suffix(s: &str) -> Option<&str> {
  213. let mut bytes = s.bytes();
  214. {
  215. // Ignore ASCII tabs or newlines like the URL parser would
  216. let iter = bytes
  217. .by_ref()
  218. .filter(|&byte| !matches!(byte, b'\t' | b'\n' | b'\r'));
  219. // Search from the end
  220. let mut iter = iter.rev();
  221. require!(iter.next()? == b'4');
  222. require!(iter.next()? == b'6');
  223. require!(iter.next()?.eq_ignore_ascii_case(&b'e'));
  224. require!(iter.next()?.eq_ignore_ascii_case(&b's'));
  225. require!(iter.next()?.eq_ignore_ascii_case(&b'a'));
  226. require!(iter.next()?.eq_ignore_ascii_case(&b'b'));
  227. require!(iter.skip_while(|&byte| byte == b' ').next()? == b';');
  228. }
  229. Some(&s[..bytes.len()])
  230. }
  231. fn percent_encode(byte: u8, string: &mut String) {
  232. const HEX_UPPER: [u8; 16] = *b"0123456789ABCDEF";
  233. string.push('%');
  234. string.push(HEX_UPPER[(byte >> 4) as usize] as char);
  235. string.push(HEX_UPPER[(byte & 0x0f) as usize] as char);
  236. }
  237. /// This is <https://url.spec.whatwg.org/#string-percent-decode> while also:
  238. ///
  239. /// * Ignoring ASCII tab or newlines
  240. /// * Stopping at the first '#' (which indicates the start of the fragment)
  241. ///
  242. /// Anything that would have been UTF-8 percent-encoded by the URL parser
  243. /// would be percent-decoded here.
  244. /// We skip that round-trip and pass it through unchanged.
  245. fn decode_without_base64<F, E>(
  246. encoded_body_plus_fragment: &str,
  247. mut write_bytes: F,
  248. ) -> Result<Option<FragmentIdentifier<'_>>, E>
  249. where
  250. F: FnMut(&[u8]) -> Result<(), E>,
  251. {
  252. let bytes = encoded_body_plus_fragment.as_bytes();
  253. let mut slice_start = 0;
  254. for (i, &byte) in bytes.iter().enumerate() {
  255. // We only need to look for 5 different "special" byte values.
  256. // For everything else we make slices as large as possible, borrowing the input,
  257. // in order to make fewer write_all() calls.
  258. if matches!(byte, b'%' | b'#' | b'\t' | b'\n' | b'\r') {
  259. // Write everything (if anything) "non-special" we’ve accumulated
  260. // before this special byte
  261. if i > slice_start {
  262. write_bytes(&bytes[slice_start..i])?;
  263. slice_start = i;
  264. }
  265. // Then deal with the special byte.
  266. match byte {
  267. b'%' => {
  268. let l = bytes.get(i + 2).and_then(|&b| (b as char).to_digit(16));
  269. let h = bytes.get(i + 1).and_then(|&b| (b as char).to_digit(16));
  270. if let (Some(h), Some(l)) = (h, l) {
  271. // '%' followed by two ASCII hex digits
  272. let one_byte = h as u8 * 0x10 + l as u8;
  273. write_bytes(&[one_byte])?;
  274. slice_start = i + 3;
  275. } else {
  276. // Do nothing. Leave slice_start unchanged.
  277. // The % sign will be part of the next slice.
  278. }
  279. }
  280. b'#' => {
  281. let fragment_start = i + 1;
  282. let fragment = &encoded_body_plus_fragment[fragment_start..];
  283. return Ok(Some(FragmentIdentifier(fragment)));
  284. }
  285. // Ignore over '\t' | '\n' | '\r'
  286. _ => slice_start = i + 1,
  287. }
  288. }
  289. }
  290. write_bytes(&bytes[slice_start..])?;
  291. Ok(None)
  292. }
  293. /// `decode_without_base64()` composed with
  294. /// <https://infra.spec.whatwg.org/#isomorphic-decode> composed with
  295. /// <https://infra.spec.whatwg.org/#forgiving-base64-decode>.
  296. fn decode_with_base64<F, E>(
  297. encoded_body_plus_fragment: &str,
  298. write_bytes: F,
  299. ) -> Result<Option<FragmentIdentifier<'_>>, forgiving_base64::DecodeError<E>>
  300. where
  301. F: FnMut(&[u8]) -> Result<(), E>,
  302. {
  303. let mut decoder = forgiving_base64::Decoder::new(write_bytes);
  304. let fragment = decode_without_base64(encoded_body_plus_fragment, |bytes| decoder.feed(bytes))?;
  305. decoder.finish()?;
  306. Ok(fragment)
  307. }