use base64::Engine as _;
use sha2::{Digest as _, Sha256};
use std::borrow::Cow;
use std::io::{self, Read as _};
const PREFIX: &str = "bcode-event-zstd:";
const VERSION: &str = "1";
pub const MAX_COMPRESSED_EVENT_BYTES: usize = 16 * 1024 * 1024;
fn payload_budget_exceeded() -> io::Error {
io::Error::new(
io::ErrorKind::OutOfMemory,
"session payload exceeds decoded byte budget",
)
}
fn invalid() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
"invalid compressed session event payload",
)
}
pub fn compress_event_payload(payload: &str, level: i32) -> io::Result<String> {
if payload.len() > MAX_COMPRESSED_EVENT_BYTES || !(1..=22).contains(&level) {
return Err(invalid());
}
serde_json::from_str::<serde_json::Value>(payload).map_err(|_| invalid())?;
let compressed = zstd::bulk::compress(payload.as_bytes(), level)?;
let encoded = format!(
"{PREFIX}{VERSION}:{}:{}:{}",
payload.len(),
hex::encode(Sha256::digest(payload.as_bytes())),
base64::engine::general_purpose::STANDARD.encode(compressed)
);
Ok(if encoded.len() < payload.len() {
encoded
} else {
payload.to_owned()
})
}
pub fn decode_event_payload(payload: &str) -> io::Result<Cow<'_, str>> {
decode_event_payload_with_limit(payload, usize::MAX)
}
pub fn decode_event_payload_with_limit(
payload: &str,
max_bytes: usize,
) -> io::Result<Cow<'_, str>> {
let Some(envelope) = payload.strip_prefix(PREFIX) else {
if payload.len() > max_bytes {
return Err(payload_budget_exceeded());
}
return Ok(Cow::Borrowed(payload));
};
let mut fields = envelope.splitn(4, ':');
if fields.next() != Some(VERSION) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"unsupported compressed event payload version",
));
}
let length = fields
.next()
.ok_or_else(invalid)?
.parse::<usize>()
.map_err(|_| invalid())?;
if length > max_bytes {
return Err(payload_budget_exceeded());
}
let checksum = fields.next().ok_or_else(invalid)?;
let encoded = fields.next().ok_or_else(invalid)?;
if length > MAX_COMPRESSED_EVENT_BYTES
|| checksum.len() != 64
|| !checksum.bytes().all(|byte| byte.is_ascii_hexdigit())
|| encoded.len() > (MAX_COMPRESSED_EVENT_BYTES + 1024) * 4 / 3 + 4
{
return Err(invalid());
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|_| invalid())?;
let mut decoder = zstd::stream::read::Decoder::new(bytes.as_slice())?;
decoder.window_log_max(24)?;
let mut expanded = Vec::with_capacity(length);
decoder.take(length as u64 + 1).read_to_end(&mut expanded)?;
if expanded.len() != length
|| !hex::encode(Sha256::digest(&expanded)).eq_ignore_ascii_case(checksum)
{
return Err(invalid());
}
String::from_utf8(expanded)
.map(Cow::Owned)
.map_err(|_| invalid())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caller_budget_limits_raw_and_compressed_expansion_before_decoding() {
let raw = serde_json::json!({"text": "repeat".repeat(10_000)}).to_string();
let compressed = compress_event_payload(&raw, 1).expect("compress");
for payload in [&raw, &compressed] {
assert_eq!(
decode_event_payload_with_limit(payload, raw.len()).expect("exact budget"),
raw
);
assert_eq!(
decode_event_payload_with_limit(payload, raw.len() - 1)
.expect_err("over budget")
.kind(),
io::ErrorKind::OutOfMemory
);
}
assert_eq!(decode_event_payload_with_limit("", 0).expect("empty"), "");
}
#[test]
fn preserves_exact_json_and_private_fields_at_both_levels() {
let original = format!(
"{{\n \"unknown\": \"{}\", \"original_usage\": {{\"private\":true}}\n}}",
"世界 terminal output ".repeat(10_000)
);
for level in [1, 12] {
let compressed = compress_event_payload(&original, level).expect("encode");
assert!(compressed.len() < original.len());
assert_eq!(decode_event_payload(&compressed).expect("decode"), original);
}
let tiny = "{\"x\":1}";
assert_eq!(compress_event_payload(tiny, 1).expect("tiny"), tiny);
assert!(matches!(
decode_event_payload(tiny).expect("borrow"),
Cow::Borrowed(_)
));
}
#[test]
fn corrupt_future_and_oversized_envelopes_fail_closed() {
let original = serde_json::json!({"text": "repeat".repeat(10_000)}).to_string();
let compressed = compress_event_payload(&original, 1).expect("encode");
assert!(decode_event_payload(&compressed.replace("zstd:1:", "zstd:2:")).is_err());
let mut corrupt = compressed.clone();
corrupt.pop();
assert!(decode_event_payload(&corrupt).is_err());
assert!(
decode_event_payload(&format!(
"{PREFIX}1:{}:{}:AAAA",
MAX_COMPRESSED_EVENT_BYTES + 1,
"0".repeat(64)
))
.is_err()
);
assert!(
decode_event_payload(&format!(
"{PREFIX}1:1:{}:{}",
"0".repeat(64),
compressed.rsplit(':').next().expect("body")
))
.is_err()
);
assert!(compress_event_payload("invalid", 1).is_err());
assert!(compress_event_payload("{}", 23).is_err());
}
}