use std::io::Read;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use flate2::bufread::GzDecoder;
pub(crate) const MAX_WIRE_BYTES: usize = 16 * 1024 * 1024;
pub(crate) const MAX_ENTITY_BYTES: usize = 16 * 1024 * 1024;
const MAX_MEMBERS: usize = 1024;
const MAX_HEADER_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BinaryBodyError {
Base64,
Gzip,
Utf8,
Limit,
}
pub(crate) struct BinaryBody {
pub(crate) text: String,
pub(crate) wire_bytes: usize,
}
#[derive(Clone, Copy)]
struct Limits {
wire: usize,
entity: usize,
members: usize,
header: usize,
}
const LIMITS: Limits = Limits {
wire: MAX_WIRE_BYTES,
entity: MAX_ENTITY_BYTES,
members: MAX_MEMBERS,
header: MAX_HEADER_BYTES,
};
pub(crate) fn decode(encoded: &str, gzip: bool) -> Result<BinaryBody, BinaryBodyError> {
decode_with_limits(encoded, gzip, LIMITS)
}
fn decode_with_limits(
encoded: &str,
gzip: bool,
limits: Limits,
) -> Result<BinaryBody, BinaryBodyError> {
if encoded.len() > limits.wire.div_ceil(3) * 4 {
return Err(BinaryBodyError::Limit);
}
let wire = STANDARD
.decode(encoded)
.map_err(|_| BinaryBodyError::Base64)?;
if wire.len() > limits.wire {
return Err(BinaryBodyError::Limit);
}
let wire_bytes = wire.len();
let entity = if gzip {
inflate(&wire, limits)?
} else {
if wire.len() > limits.entity {
return Err(BinaryBodyError::Limit);
}
wire
};
let text = String::from_utf8(entity).map_err(|_| BinaryBodyError::Utf8)?;
Ok(BinaryBody { text, wire_bytes })
}
fn inflate(wire: &[u8], limits: Limits) -> Result<Vec<u8>, BinaryBodyError> {
if wire.is_empty() {
return Err(BinaryBodyError::Gzip);
}
let mut remaining = wire;
let mut entity = Vec::new();
let mut members = 0;
while !remaining.is_empty() {
if members == limits.members {
return Err(BinaryBodyError::Limit);
}
members += 1;
check_header(remaining, limits.header)?;
let mut decoder = GzDecoder::new(remaining);
let mut chunk = [0_u8; 16 * 1024];
loop {
let available = (limits.entity - entity.len() + 1).min(chunk.len());
let read = decoder
.read(&mut chunk[..available])
.map_err(|_| BinaryBodyError::Gzip)?;
if read == 0 {
break;
}
if read > limits.entity - entity.len() {
return Err(BinaryBodyError::Limit);
}
entity.extend_from_slice(&chunk[..read]);
}
let next = decoder.into_inner();
if next.len() >= remaining.len() {
return Err(BinaryBodyError::Gzip);
}
remaining = next;
}
Ok(entity)
}
fn check_header(wire: &[u8], max: usize) -> Result<(), BinaryBodyError> {
if wire.len() < 10 || wire[..3] != [0x1f, 0x8b, 8] || wire[3] & 0xe0 != 0 {
return Err(BinaryBodyError::Gzip);
}
let flags = wire[3];
let mut offset = 10;
if flags & 4 != 0 {
checked_header_end(wire, offset + 2, max)?;
let len = u16::from_le_bytes([wire[offset], wire[offset + 1]]) as usize;
offset += 2;
checked_header_end(wire, offset + len, max)?;
offset += len;
}
for flag in [8, 16] {
if flags & flag != 0 {
let bytes = &wire[offset..];
let budget = max.saturating_sub(offset);
let end = bytes.iter().take(budget).position(|byte| *byte == 0);
match end {
Some(end) => offset += end + 1,
None if bytes.len() >= budget => return Err(BinaryBodyError::Limit),
None => return Err(BinaryBodyError::Gzip),
}
}
}
if flags & 2 != 0 {
offset += 2;
}
checked_header_end(wire, offset, max)
}
fn checked_header_end(wire: &[u8], end: usize, max: usize) -> Result<(), BinaryBodyError> {
if end > wire.len() {
Err(BinaryBodyError::Gzip)
} else if end > max {
Err(BinaryBodyError::Limit)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEN: &str = "H4sIAAAAAAAC/zMwNDI2MTUzt7AEAMbHhKYKAAAA";
fn small() -> Limits {
Limits {
wire: 128,
entity: 10,
members: 2,
header: 64,
}
}
#[test]
fn exact_entity_bound_reads_and_verifies_footer() {
assert_eq!(
decode_with_limits(TEN, true, small()).unwrap().text,
"0123456789"
);
let mut wire = STANDARD.decode(TEN).unwrap();
let footer = wire.len() - 8;
wire[footer] ^= 1;
assert_eq!(
decode_with_limits(&STANDARD.encode(wire), true, small()).err(),
Some(BinaryBodyError::Gzip)
);
}
#[test]
fn local_bounds_remain_separate_from_malformed_data() {
let mut limits = small();
limits.entity = 9;
assert_eq!(
decode_with_limits(TEN, true, limits).err(),
Some(BinaryBodyError::Limit)
);
limits = small();
limits.wire = 5;
assert_eq!(
decode_with_limits(TEN, true, limits).err(),
Some(BinaryBodyError::Limit)
);
assert_eq!(
decode_with_limits("AA=A", false, small()).err(),
Some(BinaryBodyError::Base64)
);
for encoded in ["AB==", "_w==", "AA==\n", "AA"] {
assert_eq!(
decode_with_limits(encoded, false, small()).err(),
Some(BinaryBodyError::Base64)
);
}
}
#[test]
fn header_bound_is_aggregate_and_includes_terminators() {
let original = STANDARD.decode(TEN).unwrap();
let mut wire = original[..10].to_vec();
wire[3] = 8;
wire.extend_from_slice(b"name\0");
wire.extend_from_slice(&original[10..]);
let mut limits = small();
limits.header = 15;
assert_eq!(
decode_with_limits(&STANDARD.encode(&wire), true, limits)
.unwrap()
.text,
"0123456789"
);
limits.header = 14;
assert_eq!(
decode_with_limits(&STANDARD.encode(&wire), true, limits).err(),
Some(BinaryBodyError::Limit)
);
}
#[test]
fn empty_members_count_without_inventing_entity_bytes() {
let empty = STANDARD.decode("H4sIAAAAAAAC/wMAAAAAAAAAAAA=").unwrap();
let two = [empty.as_slice(), empty.as_slice()].concat();
assert_eq!(
decode_with_limits(&STANDARD.encode(&two), true, small())
.unwrap()
.text,
""
);
let three = [two.as_slice(), empty.as_slice()].concat();
assert_eq!(
decode_with_limits(&STANDARD.encode(&three), true, small()).err(),
Some(BinaryBodyError::Limit)
);
}
#[test]
fn corrupt_later_member_never_exposes_the_valid_first_entity() {
let ten = STANDARD.decode(TEN).unwrap();
let mut second = ten.clone();
let footer = second.len() - 8;
second[footer] ^= 1;
let wire = [ten.as_slice(), second.as_slice()].concat();
let mut limits = small();
limits.entity = 20;
assert_eq!(
decode_with_limits(&STANDARD.encode(&wire), true, limits).err(),
Some(BinaryBodyError::Gzip)
);
}
#[test]
fn decoded_wire_bound_is_checked_after_encoded_length_preflight() {
let mut limits = small();
limits.wire = 4;
assert_eq!(
decode_with_limits("AAAAAA==", false, limits)
.unwrap()
.wire_bytes,
4
);
assert_eq!(
decode_with_limits("AAAAAAA=", false, limits).err(),
Some(BinaryBodyError::Limit)
);
}
}