Skip to content

Commit

Permalink
Merge #369
Browse files Browse the repository at this point in the history
369: Base64 Support r=jonasbb a=jonasbb

Create a new optional module to house the code.
Add a new `CharacterSet` trait, which is used to encode the different
character sets with multiple unique types.
Add a basic test.

Missing:
* [x] More tests for the other character sets.
* [x] Rustdoc documentation
* [x] Changelog entries
* [x] serde_as Guide entry

Closes #350

Co-authored-by: Jonas Bushart <[email protected]>
  • Loading branch information
bors[bot] and jonasbb authored Oct 4, 2021
2 parents d054cb8 + 5cb68c6 commit 5d0f9b6
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 23 deletions.
17 changes: 17 additions & 0 deletions serde_with/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

* Serialize bytes as base64 encoded strings.
The character set and padding behavior can be configured.

```rust
// Rust
#[serde_as(as = "serde_with::base64::Base64")]
value: Vec<u8>,
#[serde_as(as = "Base64<Bcrypt, Unpadded>")]
bcrypt_unpadded: Vec<u8>,

// JSON
"value": "SGVsbG8gV29ybGQ=",
"bcrypt_unpadded": "QETqZE6eT07wZEO",
```

## [1.10.0] - 2021-09-04

### Added
Expand Down
7 changes: 7 additions & 0 deletions serde_with/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ maintenance = {status = "actively-developed"}

# When adding new features update the documentation in feature-flags.md
[features]
base64 = ["base64_crate"]
chrono = ["chrono_crate"]
default = ["macros"]
guide = ["doc-comment", "macros"]
Expand All @@ -31,6 +32,7 @@ macros = ["serde_with_macros"]

# When adding new optional dependencies update the documentation in feature-flags.md
[dependencies]
base64_crate = {package = "base64", version = "0.13.0", optional = true}
chrono_crate = {package = "chrono", version = "0.4.1", features = ["serde"], optional = true}
doc-comment = {version = "0.3.3", optional = true}
hex = {version = "0.4.2", optional = true}
Expand All @@ -53,6 +55,11 @@ serde_json = {version = "1.0.25", features = ["preserve_order"]}
serde_test = "1.0.124"
version-sync = "0.9.1"

[[test]]
name = "base64"
path = "tests/base64.rs"
required-features = ["base64", "macros"]

[[test]]
name = "chrono"
path = "tests/chrono.rs"
Expand Down
203 changes: 203 additions & 0 deletions serde_with/src/base64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! De/Serialization of base64 encoded bytes
//!
//! This modules is only available when using the `base64` feature of the crate.
//!
//! Please check the documentation on the [`Base64`] type for details.
use crate::{formats, DeserializeAs, SerializeAs};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::convert::{TryFrom, TryInto};
use std::default::Default;
use std::marker::PhantomData;

/// Serialize bytes with base64
///
/// The type serializes a sequence of bytes as a base64 string.
/// It works on any type implementing `AsRef<[u8]>` for serialization and `TryFrom<Vec<u8>>` for deserialization.
///
/// The type allows customizing the character set and the padding behavior.
/// The `CHARSET` is a type implementing [`CharacterSet`].
/// `PADDING` specifies if serializing should emit padding.
/// Deserialization always supports padded and unpadded formats.
/// [`formats::Padded`] emits padding and [`formats::Unpadded`] leaves it off.
///
/// ```rust
/// # #[cfg(feature = "macros")] {
/// # use serde_derive::{Deserialize, Serialize};
/// # use serde_with::serde_as;
/// use serde_with::base64::{Base64, Bcrypt, BinHex, Standard};
/// use serde_with::formats::{Padded, Unpadded};
///
/// #[serde_as]
/// # #[derive(Debug, PartialEq, Eq)]
/// #[derive(Serialize, Deserialize)]
/// struct B64 {
/// // The default is the same as Standard character set with padding
/// #[serde_as(as = "Base64")]
/// default: Vec<u8>,
/// // Only change the character set, implies padding
/// #[serde_as(as = "Base64<BinHex>")]
/// charset_binhex: Vec<u8>,
///
/// #[serde_as(as = "Base64<Standard, Padded>")]
/// explicit_padding: Vec<u8>,
/// #[serde_as(as = "Base64<Bcrypt, Unpadded>")]
/// no_padding: Vec<u8>,
/// }
///
/// let b64 = B64 {
/// default: b"Hello World".to_vec(),
/// charset_binhex: b"Hello World".to_vec(),
/// explicit_padding: b"Hello World".to_vec(),
/// no_padding: b"Hello World".to_vec(),
/// };
/// let json = serde_json::json!({
/// "default": "SGVsbG8gV29ybGQ=",
/// "charset_binhex": "5'8VD'mI8epaD'3=",
/// "explicit_padding": "SGVsbG8gV29ybGQ=",
/// "no_padding": "QETqZE6eT07wZEO",
/// });
///
/// // Test serialization and deserialization
/// assert_eq!(json, serde_json::to_value(&b64).unwrap());
/// assert_eq!(b64, serde_json::from_value(json).unwrap());
/// # }
/// ```
// The padding might be better as `const PADDING: bool = true`
// https://blog.rust-lang.org/inside-rust/2021/09/06/Splitting-const-generics.html#featureconst_generics_default/
#[derive(Copy, Clone, Debug, Default)]
pub struct Base64<CHARSET: CharacterSet = Standard, PADDING: formats::Format = formats::Padded>(
PhantomData<(CHARSET, PADDING)>,
);

impl<T, CHARSET> SerializeAs<T> for Base64<CHARSET, formats::Padded>
where
T: AsRef<[u8]>,
CHARSET: CharacterSet,
{
fn serialize_as<S>(source: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
base64_crate::encode_config(source, base64_crate::Config::new(CHARSET::charset(), true))
.serialize(serializer)
}
}

impl<T, CHARSET> SerializeAs<T> for Base64<CHARSET, formats::Unpadded>
where
T: AsRef<[u8]>,
CHARSET: CharacterSet,
{
fn serialize_as<S>(source: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
base64_crate::encode_config(source, base64_crate::Config::new(CHARSET::charset(), false))
.serialize(serializer)
}
}

impl<'de, T, CHARSET, FORMAT> DeserializeAs<'de, T> for Base64<CHARSET, FORMAT>
where
T: TryFrom<Vec<u8>>,
CHARSET: CharacterSet,
FORMAT: formats::Format,
{
fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)
.and_then(|s| {
base64_crate::decode_config(
&*s,
base64_crate::Config::new(CHARSET::charset(), false),
)
.map_err(Error::custom)
})
.and_then(|vec: Vec<u8>| {
let length = vec.len();
vec.try_into().map_err(|_e: T::Error| {
Error::custom(format!(
"Can't convert a Byte Vector of length {} to the output type.",
length
))
})
})
}
}

/// A base64 character set from [this list](base64_crate::CharacterSet).
pub trait CharacterSet {
/// Return a specific character set.
///
/// Return one enum variant of the [`base64::CharacterSet`](base64_crate::CharacterSet) enum.
fn charset() -> base64_crate::CharacterSet;
}

/// The standard character set (uses `+` and `/`).
///
/// See [RFC 3548](https://tools.ietf.org/html/rfc3548#section-3).
#[derive(Copy, Clone, Debug, Default)]
pub struct Standard;
impl CharacterSet for Standard {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::Standard
}
}

/// The URL safe character set (uses `-` and `_`).
///
/// See [RFC 3548](https://tools.ietf.org/html/rfc3548#section-3).
#[derive(Copy, Clone, Debug, Default)]
pub struct UrlSafe;
impl CharacterSet for UrlSafe {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::UrlSafe
}
}

/// The `crypt(3)` character set (uses `./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`).
///
/// Not standardized, but folk wisdom on the net asserts that this alphabet is what crypt uses.
#[derive(Copy, Clone, Debug, Default)]
pub struct Crypt;
impl CharacterSet for Crypt {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::Crypt
}
}

/// The bcrypt character set (uses `./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`).
#[derive(Copy, Clone, Debug, Default)]
pub struct Bcrypt;
impl CharacterSet for Bcrypt {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::Bcrypt
}
}

/// The character set used in IMAP-modified UTF-7 (uses `+` and `,`).
///
/// See [RFC 3501](https://tools.ietf.org/html/rfc3501#section-5.1.3).
#[derive(Copy, Clone, Debug, Default)]
pub struct ImapMutf7;
impl CharacterSet for ImapMutf7 {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::ImapMutf7
}
}

/// The character set used in BinHex 4.0 files.
///
/// See [BinHex 4.0 Definition](http://files.stairways.com/other/binhex-40-specs-info.txt).
#[derive(Copy, Clone, Debug, Default)]
pub struct BinHex;
impl CharacterSet for BinHex {
fn charset() -> base64_crate::CharacterSet {
base64_crate::CharacterSet::BinHex
}
}
5 changes: 5 additions & 0 deletions serde_with/src/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ create_format!(
PreferOne
/// Use in combination with [`OneOrMany`](crate::OneOrMany). Always emit the list form.
PreferMany

/// Emit padding during serialization.
Padded
/// Do not emit padding during serialization.
Unpadded
);

/// Specify how lenient the deserialization process should be
Expand Down
17 changes: 12 additions & 5 deletions serde_with/src/guide/feature_flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
This crate has the following features which can be enabled.
Each entry will explain the feature in more detail.

1. [`chrono`](#chrono)
2. [`guide`](#guide)
3. [`hex`](#hex)
4. [`json`](#json)
5. [`macros`](#macros)
1. [`base64`](#base64)
2. [`chrono`](#chrono)
3. [`guide`](#guide)
4. [`hex`](#hex)
5. [`json`](#json)
6. [`macros`](#macros)

## `base64`

The `base64` feature enables serializing data in base64 format.

This pulls in `base64` as a dependency.

## `chrono`

Expand Down
57 changes: 39 additions & 18 deletions serde_with/src/guide/serde_as_transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,44 @@

This page lists the transformations implemented in this crate and supported by `serde_as`.

1. [Big Array support (Rust 1.51+)](#big-array-support-rust-151)
2. [Borrow from the input for `Cow` type](#borrow-from-the-input-for-cow-type)
3. [`Bytes` with more efficiency](#bytes-with-more-efficiency)
4. [Bytes / `Vec<u8>` to hex string](#bytes--vecu8-to-hex-string)
5. [Convert to an intermediate type using `Into`](#convert-to-an-intermediate-type-using-into)
6. [Convert to an intermediate type using `TryInto`](#convert-to-an-intermediate-type-using-tryinto)
7. [`Default` from `null`](#default-from-null)
8. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display)
9. [`Duration` as seconds](#duration-as-seconds)
10. [Ignore deserialization errors](#ignore-deserialization-errors)
11. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples)
12. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp)
13. [`None` as empty `String`](#none-as-empty-string)
14. [One or many elements into `Vec`](#one-or-many-elements-into-vec)
15. [Pick first successful deserialization](#pick-first-successful-deserialization)
16. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch)
17. [Value into JSON String](#value-into-json-string)
18. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps)
1. [Base64 encode bytes](#base64-encode-bytes)
2. [Big Array support (Rust 1.51+)](#big-array-support-rust-151)
3. [Borrow from the input for `Cow` type](#borrow-from-the-input-for-cow-type)
4. [`Bytes` with more efficiency](#bytes-with-more-efficiency)
5. [Bytes / `Vec<u8>` to hex string](#bytes--vecu8-to-hex-string)
6. [Convert to an intermediate type using `Into`](#convert-to-an-intermediate-type-using-into)
7. [Convert to an intermediate type using `TryInto`](#convert-to-an-intermediate-type-using-tryinto)
8. [`Default` from `null`](#default-from-null)
9. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display)
10. [`Duration` as seconds](#duration-as-seconds)
11. [Ignore deserialization errors](#ignore-deserialization-errors)
12. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples)
13. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp)
14. [`None` as empty `String`](#none-as-empty-string)
15. [One or many elements into `Vec`](#one-or-many-elements-into-vec)
16. [Pick first successful deserialization](#pick-first-successful-deserialization)
17. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch)
18. [Value into JSON String](#value-into-json-string)
19. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps)

## Base64 encode bytes

[`Base64`]

Requires the `base64` feature.
The character set and padding behavior can be configured.

```ignore
// Rust
#[serde_as(as = "serde_with::base64::Base64")]
value: Vec<u8>,
#[serde_as(as = "Base64<Bcrypt, Unpadded>")]
bcrypt_unpadded: Vec<u8>,
// JSON
"value": "SGVsbG8gV29ybGQ=",
"bcrypt_unpadded": "QETqZE6eT07wZEO",
```

## Big Array support (Rust 1.51+)

Expand Down Expand Up @@ -339,6 +359,7 @@ This includes `BinaryHeap<(K, V)>`, `BTreeSet<(K, V)>`, `HashSet<(K, V)>`, `Link

The [inverse operation](#maps-to-vec-of-tuples) is also available.

[`Base64`]: crate::base64::Base64
[`Bytes`]: crate::Bytes
[`chrono::DateTime<Local>`]: chrono_crate::DateTime
[`chrono::DateTime<Utc>`]: chrono_crate::DateTime
Expand Down
3 changes: 3 additions & 0 deletions serde_with/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@
#[doc(hidden)]
pub extern crate serde;

#[cfg(feature = "base64")]
#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
pub mod base64;
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
pub mod chrono;
Expand Down
Loading

0 comments on commit 5d0f9b6

Please sign in to comment.