Skip to content

Commit

Permalink
Add "HumanBinaryData" as an alternative to "Base64UrlSafeData" (#354)
Browse files Browse the repository at this point in the history
* Add "HumanBinaryData" as alternative to "Base64UrlSafeData" (#352)

* Add bytes support to Base64UrlSafeData, and copy across the tests

* Rework the docs

* dedupe functionality into macros, make tests consistent

* more tests and conversions

* move Borrow impl into common

* add some more vec features

* fix clippy
  • Loading branch information
micolous authored Oct 2, 2023
1 parent 1f85ef4 commit c20e83f
Show file tree
Hide file tree
Showing 5 changed files with 560 additions and 100 deletions.
4 changes: 4 additions & 0 deletions base64urlsafedata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ readme = "README.md"
[dependencies]
serde.workspace = true
base64.workspace = true
paste = "1.0.14"

[dev-dependencies]
serde_cbor_2.workspace = true
serde_json.workspace = true
176 changes: 176 additions & 0 deletions base64urlsafedata/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/// Macro to declare common functionality for [`Base64UrlSafeData`][0] and
/// [`HumanBinaryData`][1]
///
/// [0]: crate::Base64UrlSafeData
/// [1]: crate::HumanBinaryData
macro_rules! common_impls {
($type:ty) => {
impl $type {
pub const fn new() -> Self {
Self(Vec::new())
}

pub fn with_capacity(capacity: usize) -> Self {
Vec::with_capacity(capacity).into()
}
}

impl std::ops::Deref for $type {
type Target = Vec<u8>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::DerefMut for $type {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl std::borrow::Borrow<[u8]> for $type {
fn borrow(&self) -> &[u8] {
self.0.as_slice()
}
}

impl From<Vec<u8>> for $type {
fn from(value: Vec<u8>) -> Self {
Self(value)
}
}

impl<const N: usize> From<[u8; N]> for $type {
fn from(value: [u8; N]) -> Self {
Self(value.to_vec())
}
}

impl From<&[u8]> for $type {
fn from(value: &[u8]) -> Self {
Self(value.to_vec())
}
}

impl<const N: usize> From<&[u8; N]> for $type {
fn from(value: &[u8; N]) -> Self {
Self(value.to_vec())
}
}

impl From<$type> for Vec<u8> {
fn from(value: $type) -> Self {
value.0
}
}

impl AsRef<[u8]> for $type {
fn as_ref(&self) -> &[u8] {
&self.0
}
}

macro_rules! partial_eq_impl {
($other:ty) => {
impl PartialEq<$other> for $type {
fn eq(&self, other: &$other) -> bool {
self.as_slice() == &other[..]
}
}

impl PartialEq<$type> for $other {
fn eq(&self, other: &$type) -> bool {
self.eq(&other.0)
}
}
};
}

partial_eq_impl!(Vec<u8>);
partial_eq_impl!([u8]);
partial_eq_impl!(&[u8]);

impl<const N: usize> PartialEq<[u8; N]> for $type {
fn eq(&self, other: &[u8; N]) -> bool {
self.0.eq(other)
}
}

impl<const N: usize> PartialEq<$type> for [u8; N] {
fn eq(&self, other: &$type) -> bool {
self.as_slice().eq(&other.0)
}
}

paste! {
#[doc(hidden)]
struct [<$type Visitor>];

impl<'de> serde::de::Visitor<'de> for [<$type Visitor>] {
type Value = $type;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"a url-safe base64-encoded string, bytes, or sequence of integers"
)
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
// Forgive alt base64 decoding formats
for config in crate::ALLOWED_DECODING_FORMATS {
if let Ok(data) = config.decode(v) {
return Ok(<$type>::from(data));
}
}

Err(serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self))
}

fn visit_seq<A>(self, mut v: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut data = if let Some(sz) = v.size_hint() {
Vec::with_capacity(sz)
} else {
Vec::new()
};

while let Some(i) = v.next_element()? {
data.push(i)
}
Ok(<$type>::from(data))
}

fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(<$type>::from(v))
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(<$type>::from(v))
}
}

impl<'de> serde::Deserialize<'de> for $type {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as serde::Deserializer<'de>>::Error>
where
D: serde::Deserializer<'de>,
{
// Was previously _str
deserializer.deserialize_any([<$type Visitor>])
}
}
}
};
}
49 changes: 49 additions & 0 deletions base64urlsafedata/src/human.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::fmt;

use crate::{Base64UrlSafeData, URL_SAFE_NO_PAD};
use base64::Engine;
use serde::{Serialize, Serializer};

/// Serde wrapper for `Vec<u8>` which emits URL-safe, non-padded Base64 for
/// *only* human-readable formats, and accepts Base64 and binary formats.
///
/// * Deserialisation is described in the [module documentation][crate].
///
/// * Serialisation to [a human-readable format][0] (such as JSON) emits
/// URL-safe, non-padded Base64 (per [RFC 4648 §5][sec5]).
///
/// * Serialisation to [a non-human-readable format][0] (such as CBOR) emits
/// a native "bytes" type, and not encode the value.
///
/// [0]: https://docs.rs/serde/latest/serde/trait.Serializer.html#method.is_human_readable
/// [sec5]: https://datatracker.ietf.org/doc/html/rfc4648#section-5
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct HumanBinaryData(Vec<u8>);

common_impls!(HumanBinaryData);

impl From<Base64UrlSafeData> for HumanBinaryData {
fn from(value: Base64UrlSafeData) -> Self {
Self(value.into())
}
}

impl PartialEq<Base64UrlSafeData> for HumanBinaryData {
fn eq(&self, other: &Base64UrlSafeData) -> bool {
self.0.eq(other)
}
}

impl Serialize for HumanBinaryData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
let encoded = URL_SAFE_NO_PAD.encode(self);
serializer.serialize_str(&encoded)
} else {
serializer.serialize_bytes(self)
}
}
}
Loading

0 comments on commit c20e83f

Please sign in to comment.