#![cfg_attr(not(test), no_std)]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
pub use base64;
pub use percent_encoding;
pub use percent_encoding::{AsciiSet, CONTROLS, NON_ALPHANUMERIC};
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use percent_encoding::{percent_decode_str, utf8_percent_encode};
pub fn base64_encode(bytes: &[u8]) -> String {
STANDARD.encode(bytes)
}
pub fn base64_decode(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
STANDARD.decode(encoded)
}
const COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~');
pub fn percent_encode(s: &str) -> String {
utf8_percent_encode(s, COMPONENT).to_string()
}
pub fn percent_encode_into(out: &mut String, s: &str) {
out.extend(utf8_percent_encode(s, COMPONENT));
}
pub fn percent_encode_set(s: &str, set: &'static AsciiSet) -> String {
utf8_percent_encode(s, set).to_string()
}
pub fn percent_decode(s: &str, plus_as_space: bool) -> String {
if plus_as_space && s.contains('+') {
let replaced = s.replace('+', " ");
percent_decode_str(&replaced)
.decode_utf8_lossy()
.into_owned()
} else {
percent_decode_str(s).decode_utf8_lossy().into_owned()
}
}
pub mod base64_bytes {
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&super::base64_encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
let encoded = String::deserialize(deserializer)?;
super::base64_decode(&encoded).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_round_trips() {
for input in [b"".as_slice(), b"a", b"hello world", &[0u8, 255, 128, 1]] {
let encoded = base64_encode(input);
assert_eq!(base64_decode(&encoded).unwrap(), input);
}
}
#[test]
fn known_base64_vectors() {
assert_eq!(base64_encode(b"hi"), "aGk=");
assert_eq!(base64_decode("aGk=").unwrap(), b"hi");
}
#[test]
fn invalid_base64_is_rejected() {
assert!(base64_decode("not valid base64!!!").is_err());
}
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct Chunk {
#[serde(with = "base64_bytes", default, skip_serializing_if = "Vec::is_empty")]
payload: Vec<u8>,
}
#[test]
fn percent_encode_keeps_unreserved_and_escapes_the_rest() {
assert_eq!(percent_encode("AZaz09-._~"), "AZaz09-._~");
assert_eq!(percent_encode("a b/c?d#e"), "a%20b%2Fc%3Fd%23e");
assert_eq!(percent_encode("é"), "%C3%A9");
}
#[test]
fn percent_encode_into_appends() {
let mut out = String::from("p/");
percent_encode_into(&mut out, "a b");
assert_eq!(out, "p/a%20b");
}
#[test]
fn percent_encode_set_honors_custom_set() {
const SPACE_ONLY: &AsciiSet = &CONTROLS.add(b' ');
assert_eq!(percent_encode_set("a b/c~d", SPACE_ONLY), "a%20b/c~d");
assert_eq!(percent_encode_set("a-b.c", NON_ALPHANUMERIC), "a%2Db%2Ec");
}
#[test]
fn percent_decode_round_trips_component() {
for input in ["", "plain", "a b/c?d#e", "é", "100%done"] {
assert_eq!(percent_decode(&percent_encode(input), false), input);
}
}
#[test]
fn percent_decode_plus_semantics() {
assert_eq!(percent_decode("a+b%20c", true), "a b c");
assert_eq!(percent_decode("a+b%20c", false), "a+b c");
}
#[test]
fn percent_decode_is_lossy_and_passes_through_stray_percent() {
assert_eq!(percent_decode("%FF", false), "\u{FFFD}");
assert_eq!(percent_decode("100%zz", false), "100%zz");
}
#[test]
fn serde_adapter_round_trips_and_omits_empty() {
let chunk = Chunk {
payload: alloc::vec![1, 2, 3, 4],
};
let json = serde_json::to_string(&chunk).unwrap();
assert!(json.contains("AQIDBA=="));
assert_eq!(serde_json::from_str::<Chunk>(&json).unwrap(), chunk);
let empty = Chunk {
payload: Vec::new(),
};
let json = serde_json::to_string(&empty).unwrap();
assert_eq!(json, "{}");
assert_eq!(serde_json::from_str::<Chunk>("{}").unwrap(), empty);
}
}