diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7f1a9..bff2141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.3.0 (unreleased) + +### Added + + - `dataurl::mime::MimeType` (also exported as `dataurl::MimeType`): the WHATWG MIME type parser used for + data URLs, available on its own, e.g. for `Content-Type` headers + - `DataUrl::set_mime_type()`, which sets the media type and all of its parameters from a `MimeType` + - `DataEncoding::Shortest`, which picks percent-encoding or base64, whichever results in a shorter data URL + - `Escaping::Minimal`, which only escapes what has to be when percent-encoding, for data URLs that go inside + double-quoted HTML attributes or CSS strings; this keeps text formats like SVG short and readable + +### Changed + + - `DataUrl::mime_type()` returns a `MimeType` instead of a `String` (use `.to_string()` for the old result) + - `is_base64_encoded()` and `set_is_base64_encoded()` are replaced by `data_encoding()` and `set_data_encoding()`, + which use the new `DataEncoding` enum (`Percent`, `Base64` or `Shortest`) + - Percent-encoding now also escapes `&`, so that HTML entities can't alter the data + when a data URL is put into an HTML attribute by hand + ## 0.2.1 ### Added diff --git a/README.md b/README.md index 4087f04..13f8270 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,20 @@ cat file.png | dataurl -b ```toml [dependencies] -dataurl = { version = "0.2", default-features = false } +dataurl = { version = "0.3", default-features = false } ``` ```rust -use dataurl::DataUrl; +use dataurl::{DataEncoding, DataUrl}; let data_url: DataUrl = "data:,Hello,%20World!".parse()?; assert_eq!(data_url.media_type(), "text/plain"); assert_eq!(data_url.media_type_no_default(), None); -assert_eq!(data_url.mime_type(), "text/plain;charset=US-ASCII"); +assert_eq!(data_url.mime_type().to_string(), "text/plain;charset=US-ASCII"); assert_eq!(data_url.charset(), "US-ASCII"); assert_eq!(data_url.charset_no_default(), None); -assert!(!data_url.is_base64_encoded()); +assert_eq!(data_url.data_encoding(), DataEncoding::Percent); assert_eq!(data_url.data(), b"Hello, World!"); assert_eq!(data_url.fragment(), None); assert_eq!(data_url.text(), "Hello, World!"); @@ -74,7 +74,7 @@ assert_eq!(data_url.to_string(), "data:,Hello,%20World!"); let mut data_url = DataUrl::new(); data_url.set_media_type(Some("image/png"))?; -data_url.set_is_base64_encoded(true); +data_url.set_data_encoding(DataEncoding::Base64); data_url.set_data(&[0x89, 0x50, 0x4E, 0x47]); assert_eq!(data_url.to_string(), "data:image/png;base64,iVBORw=="); ``` @@ -82,6 +82,46 @@ assert_eq!(data_url.to_string(), "data:image/png;base64,iVBORw=="); Data is stored as raw bytes and serialized as-is; use `set_text()` to store text encoded in the data URL's charset, and `text()` to decode it. Disabling default features skips the `cli` feature, which only matters for `cargo install`. +### Choosing how data gets encoded + +`set_data_encoding()` picks percent-encoding (the default), base64, or `DataEncoding::Shortest`, whichever comes out shorter for the data. +`set_escaping()` controls how much percent-encoding escapes: `Escaping::Full` (the default) makes the data URL safe anywhere, +while `Escaping::Minimal` only escapes what has to be, which keeps text formats like SVG short and readable, +for data URLs that go inside double-quoted HTML attributes or CSS strings: + +```rust +use dataurl::{DataEncoding, DataUrl, Escaping}; + +let mut data_url = DataUrl::new(); +data_url.set_media_type(Some("image/svg+xml"))?; +data_url.set_data_encoding(DataEncoding::Shortest); +data_url.set_escaping(Escaping::Minimal); +data_url.set_data(b""); + +assert_eq!( + data_url.to_string(), + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle r='4'/%3E%3C/svg%3E" +); +``` + +### MIME types + +The WHATWG MIME type parser used for data URLs is available on its own, e.g. for `Content-Type` headers: + +```rust +use dataurl::{DataUrl, MimeType}; + +let mime_type: MimeType = "Text/CSS; charset=\"UTF-8\"".parse()?; +assert_eq!(mime_type.essence(), "text/css"); +assert_eq!(mime_type.charset(), Some("UTF-8")); + +let mut data_url = DataUrl::new(); +data_url.set_mime_type(Some(&mime_type))?; +assert_eq!(data_url.to_string(), "data:text/css;charset=UTF-8,"); +``` + +### Media type detection + Media types can be detected from the data's signature, falling back to the file extension (which is what text formats like CSS and JavaScript need): @@ -93,7 +133,7 @@ assert_eq!(detect_media_type(b"body{}", Some("style.css")), Some("text/css")); ``` If all you need is parsing, [data-url](https://crates.io/crates/data-url) (from the Servo project) is another spec-compliant option; -dataurl also generates data URLs, and comes with a CLI tool. +dataurl also generates data URLs (picking the shortest encoding if asked to), handles charsets, detects media types, and comes with a CLI tool. --------------------------------------------------- diff --git a/src/lib.rs b/src/lib.rs index 8890a5b..d41bb89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,20 +6,31 @@ //! forgiving base64, and MIME type parsing. //! //! ``` -//! use dataurl::DataUrl; +//! use dataurl::{DataEncoding, DataUrl, Escaping}; //! //! let data_url: DataUrl = "data:,Hello,%20World!".parse()?; //! assert_eq!(data_url.media_type(), "text/plain"); -//! assert_eq!(data_url.mime_type(), "text/plain;charset=US-ASCII"); +//! assert_eq!(data_url.mime_type().to_string(), "text/plain;charset=US-ASCII"); //! assert_eq!(data_url.charset(), "US-ASCII"); //! assert_eq!(data_url.data(), b"Hello, World!"); //! assert_eq!(data_url.text(), "Hello, World!"); //! //! let mut data_url = DataUrl::new(); //! data_url.set_media_type(Some("image/png"))?; -//! data_url.set_is_base64_encoded(true); +//! data_url.set_data_encoding(DataEncoding::Base64); //! data_url.set_data(&[0x89, 0x50, 0x4E, 0x47]); //! assert_eq!(data_url.to_string(), "data:image/png;base64,iVBORw=="); +//! +//! // For text, percent-encoding with minimal escaping is often much shorter than base64 +//! let mut data_url = DataUrl::new(); +//! data_url.set_media_type(Some("image/svg+xml"))?; +//! data_url.set_data_encoding(DataEncoding::Shortest); +//! data_url.set_escaping(Escaping::Minimal); +//! data_url.set_data(b""); +//! assert_eq!( +//! data_url.to_string(), +//! "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle r='4'/%3E%3C/svg%3E" +//! ); //! # Ok::<(), dataurl::Error>(()) //! ``` //! @@ -27,10 +38,11 @@ mod base64; mod detect; -mod mime; +pub mod mime; mod percent; pub use detect::detect_media_type; +pub use mime::MimeType; use encoding_rs::{Encoding, REPLACEMENT, UTF_16BE, UTF_16LE, WINDOWS_1252}; use std::borrow::Cow; @@ -92,6 +104,39 @@ impl fmt::Display for Error { impl std::error::Error for Error {} +/// How the data part of a data URL gets encoded when serializing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +#[non_exhaustive] +pub enum DataEncoding { + /// Percent-encoding (`data:,Hello%2C%20World`), which keeps text readable. + #[default] + Percent, + /// Base64 (`data:;base64,SGVsbG8sIFdvcmxk`), which is more compact for binary data. + Base64, + /// Whichever of the two results in a shorter data URL, given the [`Escaping`] in use. + /// Parsing never yields this value, since a parsed data URL is either one or the other. + Shortest, +} + +/// How much of the data gets escaped when it's percent-encoded. +/// +/// Some characters always get escaped, whichever option is used: those that can't appear in +/// a data URL (controls, non-ASCII, `%`, `#`), those that would end or alter a double-quoted +/// HTML attribute, a CSS string, or a `