use std::io::{Read, Write};
use data_encoding::BASE64URL_NOPAD;
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum BlobError {
#[error("invalid blob encoding: {0}")]
Encoding(#[from] data_encoding::DecodeError),
#[error("invalid blob format: {0}")]
Format(#[from] postcard::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(transparent)]
pub struct Blob(String);
impl Blob {
pub fn encode(bytes: &[u8]) -> Self {
let postcard_bytes =
postcard::to_allocvec(bytes).expect("blob serialization should not fail");
Self(BASE64URL_NOPAD.encode(&postcard_bytes))
}
pub fn decode(&self) -> Result<Vec<u8>, BlobError> {
let postcard_bytes = BASE64URL_NOPAD.decode(self.0.as_bytes())?;
Ok(postcard::from_bytes(&postcard_bytes)?)
}
pub fn decompressed(&self) -> Result<Vec<u8>, BlobError> {
let compressed_bytes = self.decode()?;
let mut decoder = ZlibDecoder::new(&compressed_bytes[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
pub fn compressed(bytes: &[u8]) -> Result<Self, BlobError> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(bytes)?;
let compressed_bytes = encoder.finish()?;
Ok(Self::encode(&compressed_bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blob_roundtrip() {
let original = b"hello world, this is binary \x00\xff data";
let blob = Blob::encode(original);
let decoded = blob.decode().unwrap();
assert_eq!(original.as_slice(), &decoded);
}
#[test]
fn blob_serde_as_string() {
let blob = Blob::encode(b"test data");
let json = serde_json::to_value(&blob).unwrap();
assert!(json.is_string(), "should serialize as plain string");
let decoded: Blob = serde_json::from_value(json).unwrap();
assert_eq!(blob.decode().unwrap(), decoded.decode().unwrap());
}
#[test]
fn blob_empty_roundtrip() {
let blob = Blob::encode(b"");
let decoded = blob.decode().unwrap();
assert!(decoded.is_empty());
}
#[test]
fn blob_invalid_base64_fails() {
let blob = Blob("!!!invalid!!!".into());
assert!(blob.decode().is_err());
}
}