use std::fmt;
use base64::Engine as _;
use base64::alphabet::STANDARD;
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{DecodePaddingMode, GeneralPurpose};
use serde::de::{self, Deserializer, SeqAccess, Visitor};
use serde::ser::Serializer;
const ENGINE: GeneralPurpose = GeneralPurpose::new(
&STANDARD,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
const EXPECTED: &str = "payload bytes as a base64 string or an array of integers 0..=255";
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_seq(bytes)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
deserializer.deserialize_any(BytesVisitor)
} else {
deserializer.deserialize_seq(BytesVisitor)
}
}
pub fn from_json_value(value: &serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
deserialize(value)
}
#[must_use]
pub fn decoded_len(value: &serde_json::Value) -> Option<u64> {
match value {
serde_json::Value::Array(bytes) => u64::try_from(bytes.len()).ok(),
serde_json::Value::String(text) => {
let unpadded = text.trim_end_matches('=').len();
u64::try_from(unpadded * 6 / 8).ok()
}
_ => None,
}
}
struct BytesVisitor;
impl<'de> Visitor<'de> for BytesVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(EXPECTED)
}
fn visit_str<E>(self, text: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
ENGINE.decode(text).map_err(|error| {
E::custom(format!(
"payload bytes are a string but not base64 in the standard alphabet ({error}); \
expected {EXPECTED}"
))
})
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(bytes.to_vec())
}
fn visit_byte_buf<E>(self, bytes: Vec<u8>) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(bytes)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(element) = seq.next_element::<serde_json::Value>()? {
let index = bytes.len();
let byte = match element.as_u64().map(u8::try_from) {
Some(Ok(byte)) => byte,
Some(Err(_)) => {
return Err(de::Error::custom(format!(
"payload byte {index} is {element}, outside 0..=255; expected {EXPECTED}"
)));
}
None => {
return Err(de::Error::custom(format!(
"payload byte {index} is {}, not an integer; expected {EXPECTED}",
json_kind(&element)
)));
}
};
bytes.push(byte);
}
Ok(bytes)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut keys = Vec::new();
while let Some((key, _)) = map.next_entry::<String, serde::de::IgnoredAny>()? {
keys.push(key);
}
let what = if keys.iter().any(|key| key == "__elided") {
"an elided projection (`__elided`), which carries no bytes"
} else {
"an object"
};
Err(de::Error::custom(format!(
"payload bytes are {what}; expected {EXPECTED}"
)))
}
}
fn json_kind(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(number) if number.is_f64() => "a fraction",
serde_json::Value::Number(_) => "a negative integer",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct Carrier {
#[serde(with = "super")]
bytes: Vec<u8>,
}
#[test]
fn a_base64_string_decodes_with_or_without_padding() -> Result<(), serde_json::Error> {
let padded: Carrier = serde_json::from_value(json!({ "bytes": "eyJvayI6dHJ1ZX0=" }))?;
let bare: Carrier = serde_json::from_value(json!({ "bytes": "eyJvayI6dHJ1ZX0" }))?;
assert_eq!(padded.bytes, br#"{"ok":true}"#);
assert_eq!(bare, padded);
Ok(())
}
#[test]
fn an_integer_array_decodes() -> Result<(), serde_json::Error> {
let carrier: Carrier = serde_json::from_value(json!({ "bytes": [0, 15, 16, 255] }))?;
assert_eq!(carrier.bytes, vec![0, 15, 16, 255]);
let empty: Carrier = serde_json::from_value(json!({ "bytes": [] }))?;
assert!(empty.bytes.is_empty());
Ok(())
}
#[test]
fn the_writer_still_emits_the_array_until_every_reader_has_moved()
-> Result<(), serde_json::Error> {
let value = serde_json::to_value(Carrier {
bytes: vec![123, 125],
})?;
assert_eq!(value, json!({ "bytes": [123, 125] }));
Ok(())
}
#[test]
fn every_refusal_names_the_shape_it_found() {
let cases: [(serde_json::Value, &[&str]); 6] = [
(
json!({ "bytes": [256] }),
&["payload byte 0 is 256", "outside 0..=255"],
),
(
json!({ "bytes": [1, "a"] }),
&["payload byte 1 is a string", "not an integer"],
),
(
json!({ "bytes": [1, -1] }),
&["payload byte 1 is a negative integer"],
),
(
json!({ "bytes": "not-base64!" }),
&["a string but not base64"],
),
(
json!({ "bytes": { "__elided": true, "size_bytes": 9 } }),
&["an elided projection", "carries no bytes"],
),
(
json!({ "bytes": 7 }),
&["invalid type: integer `7`", "expected payload bytes"],
),
];
for (value, words) in cases {
let refused = serde_json::from_value::<Carrier>(value.clone());
assert!(refused.is_err(), "{value} must be refused");
let message = refused
.err()
.map(|error| error.to_string())
.unwrap_or_default();
for word in words {
assert!(
message.contains(word),
"{value}: refusal {message:?} lacks {word:?}"
);
}
}
}
#[test]
fn decoded_len_measures_both_shapes_without_decoding() {
assert_eq!(super::decoded_len(&json!([1, 2, 3, 4, 5])), Some(5));
assert_eq!(super::decoded_len(&json!([])), Some(0));
assert_eq!(super::decoded_len(&json!("aGVsbG8=")), Some(5));
assert_eq!(super::decoded_len(&json!("aGVsbA==")), Some(4));
assert_eq!(super::decoded_len(&json!("aGVsbA")), Some(4));
assert_eq!(super::decoded_len(&json!("aGVs")), Some(3));
assert_eq!(super::decoded_len(&json!("")), Some(0));
assert_eq!(
super::decoded_len(&json!({ "__elided": true, "size_bytes": 9 })),
None
);
assert_eq!(super::decoded_len(&json!(null)), None);
}
#[test]
fn the_hand_reader_and_the_typed_path_agree() -> Result<(), serde_json::Error> {
let string = super::from_json_value(&json!("eyJvayI6dHJ1ZX0="))?;
let array =
super::from_json_value(&json!([123, 34, 111, 107, 34, 58, 116, 114, 117, 101, 125]))?;
assert_eq!(string, array);
assert!(super::from_json_value(&json!(null)).is_err());
Ok(())
}
}