Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
run: cargo build --all --locked --verbose
- name: Run tests
run: cargo test --all --locked --verbose
- name: Run tests with all features
run: cargo test --all --all-features --locked --verbose

# The minimum supported Rust version, as declared by rust-version in Cargo.toml
msrv:
Expand All @@ -52,7 +54,7 @@ jobs:
sed -i 's/^version = 4$/version = 3/' Cargo.lock
grep -A1 'name = "encoding_rs"' Cargo.lock
- name: Build and run tests with Rust 1.74
run: cargo +1.74 test --locked --verbose
run: cargo +1.74 test --all-features --locked --verbose

lint:
runs-on: ubuntu-latest
Expand All @@ -68,8 +70,8 @@ jobs:
cargo fmt --all -- --check
rustfmt --edition 2021 --check fuzz/fuzz_targets/*.rs
- name: Run clippy
run: cargo clippy --all-targets --locked -- -D warnings
run: cargo clippy --all-targets --all-features --locked -- -D warnings
- name: Check documentation
run: cargo doc --no-deps --locked
run: cargo doc --no-deps --all-features --locked
env:
RUSTDOCFLAGS: -D warnings
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Added

- `serde` feature (off by default), which implements `Serialize` and `Deserialize` for `DataUrl` and `MimeType`,
as strings; invalid data URLs and MIME types make deserializing fail

## 0.3.0

### Added
Expand Down
75 changes: 75 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ license = "CC0-1.0"
[dependencies]
encoding_rs = "0.8.35" # Used for charsets (label validation, decoding and encoding text)
infer = { version = "0.22.0", default-features = false } # Used for detecting media types by file signature
serde = { version = "1.0", default-features = false, optional = true } # Used for the optional serde support

[dev-dependencies]
serde_test = "1.0.177" # Used for testing the serde support

[features]
default = ["cli"]
cli = [] # Build the dataurl executable (no extra dependencies, kept for compatibility)
serde = ["dep:serde"] # Implement Serialize and Deserialize for DataUrl and MimeType

[package.metadata.docs.rs]
all-features = true

[[bin]]
name = "dataurl"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build:
.PHONY: build

test: build
@cargo test --locked
@cargo test --locked --all-features
@cargo fmt --all -- --check
.PHONY: test

Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,29 @@ assert_eq!(extension_for_media_type("application/javascript"), Some("js"));

The full API is documented on [docs.rs](https://docs.rs/dataurl).

### Serde

With the `serde` feature, `DataUrl` and `MimeType` implement `Serialize` and `Deserialize`, as strings,
so they can be part of configuration files, API responses and so on:

```toml
[dependencies]
dataurl = { version = "0.3", default-features = false, features = ["serde"] }
```

```rust
use dataurl::DataUrl;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Theme {
name: String,
logo: DataUrl, // "data:image/svg+xml,..." in JSON, TOML, YAML…
}
```

Invalid data URLs and MIME types make deserializing fail.

### Comparison with data-url

[data-url](https://crates.io/crates/data-url), from the Servo project (it lives in the [rust-url](https://github.com/servo/rust-url) repository),
Expand Down Expand Up @@ -199,7 +222,7 @@ If you need to create or modify data URLs, work with text in other charsets, or
make test
```

Besides unit tests, this runs the [Web Platform Tests](https://github.com/web-platform-tests/wpt) suites for data URLs,
Besides unit tests (including the ones for the `serde` feature), this runs the [Web Platform Tests](https://github.com/web-platform-tests/wpt) suites for data URLs,
base64 and MIME types (in `tests/wpt/`), and randomized round-trip tests (`tests/roundtrip.rs`).
Those run 5000 iterations each by default; set `DATAURL_ROUNDTRIP_ITERATIONS` for more.
The same checks are available as [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) targets, which need a nightly toolchain:
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@
//! decoding text in other charsets, and detecting media types
//! (see the [comparison](https://github.com/Y2Z/dataurl#comparison-with-data-url)).
//!
//! With the `serde` feature, [`DataUrl`] and [`MimeType`] implement `Serialize` and `Deserialize`,
//! as strings.
//!
//! [data: URL processor]: https://fetch.spec.whatwg.org/#data-url-processor
//! [data-url]: https://crates.io/crates/data-url

mod base64;
mod detect;
pub mod mime;
mod percent;
#[cfg(feature = "serde")]
mod serde_impl;

pub use detect::{detect_media_type, extension_for_media_type};
pub use mime::MimeType;
Expand Down
69 changes: 69 additions & 0 deletions src/serde_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Serde support (the `serde` feature): data URLs and MIME types are serialized as strings,
//! and deserialized by parsing those strings.

use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;

use serde::de::{self, Deserialize, Deserializer, Visitor};
use serde::ser::{Serialize, Serializer};

use crate::{DataUrl, Error, MimeType};

/// Serialized as its string form (the same as `to_string()`, written without building a string first
/// by serializers that support that, such as serde_json's).
impl Serialize for DataUrl {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}

/// Deserialized from a string with [`DataUrl::parse()`]; invalid data URLs are an error.
impl<'de> Deserialize<'de> for DataUrl {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(ParsingVisitor::new("a data URL"))
}
}

/// Serialized as its string form (the same as `to_string()`).
impl Serialize for MimeType {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}

/// Deserialized from a string with [`MimeType::parse()`]; invalid MIME types are an error.
impl<'de> Deserialize<'de> for MimeType {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(ParsingVisitor::new("a MIME type"))
}
}

struct ParsingVisitor<T> {
expecting: &'static str,
marker: PhantomData<T>,
}

impl<T> ParsingVisitor<T> {
fn new(expecting: &'static str) -> Self {
ParsingVisitor {
expecting,
marker: PhantomData,
}
}
}

impl<T: FromStr<Err = Error>> Visitor<'_> for ParsingVisitor<T> {
type Value = T;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(self.expecting)
}

fn visit_str<E: de::Error>(self, value: &str) -> Result<T, E> {
// The input isn't included in the error, since data URLs can be huge
value
.parse()
.map_err(|error| E::custom(format_args!("expected {}: {error}", self.expecting)))
}
}
77 changes: 77 additions & 0 deletions tests/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! The serde feature (run with `cargo test --features serde`)
#![cfg(feature = "serde")]

use dataurl::{DataEncoding, DataUrl, MimeType};
use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token};

#[test]
fn data_url_as_string() {
let data_url = DataUrl::parse("data:text/plain;charset=utf-8,Hello,%20World!").unwrap();
assert_tokens(
&data_url,
&[Token::Str("data:text/plain;charset=utf-8,Hello,%20World!")],
);
}

#[test]
fn data_url_keeps_base64() {
let data_url: DataUrl = "data:image/png;base64,iVBORw==".parse().unwrap();
assert_eq!(data_url.data_encoding(), DataEncoding::Base64);
assert_tokens(&data_url, &[Token::Str("data:image/png;base64,iVBORw==")]);
}

#[test]
fn data_url_from_borrowed_and_owned_strings() {
let data_url = DataUrl::parse("data:,x").unwrap();
assert_de_tokens(&data_url, &[Token::BorrowedStr("data:,x")]);
assert_de_tokens(&data_url, &[Token::String("data:,x")]);
}

#[test]
fn data_url_gets_normalized() {
// Deserializing parses the string, so what gets serialized again is the normalized form
let data_url: DataUrl = "DATA:Text/Plain;Charset=\"UTF-8\",x".parse().unwrap();
assert_de_tokens(
&data_url,
&[Token::Str("DATA:Text/Plain;Charset=\"UTF-8\",x")],
);
assert_tokens(&data_url, &[Token::Str("data:text/plain;charset=UTF-8,x")]);
}

#[test]
fn invalid_data_url() {
assert_de_tokens_error::<DataUrl>(
&[Token::Str("https://example.com/")],
"expected a data URL: not a data URL",
);
assert_de_tokens_error::<DataUrl>(
&[Token::Str("data:;base64,a")],
"expected a data URL: data URL contains invalid base64",
);
}

#[test]
fn data_url_must_be_a_string() {
assert_de_tokens_error::<DataUrl>(
&[Token::U32(1)],
"invalid type: integer `1`, expected a data URL",
);
}

#[test]
fn mime_type_as_string() {
let mime_type = MimeType::parse("Text/HTML; charset=\"Shift_JIS\"").unwrap();
assert_tokens(&mime_type, &[Token::Str("text/html;charset=Shift_JIS")]);
assert_de_tokens(
&mime_type,
&[Token::Str("Text/HTML; charset=\"Shift_JIS\"")],
);
}

#[test]
fn invalid_mime_type() {
assert_de_tokens_error::<MimeType>(
&[Token::Str("text")],
"expected a MIME type: invalid media type",
);
}
Loading