#![forbid(unsafe_code)]
use crate::decoders::StreamDecoder;
use crate::error::{Error, Result};
use flate2::read::{DeflateDecoder, ZlibDecoder};
use std::io::Read;
pub const DEFAULT_MAX_DECOMPRESSED_BYTES: u64 = 256 * 1024 * 1024;
fn effective_limit_from_str(val: Option<&str>) -> u64 {
val.and_then(|v| v.parse::<u64>().ok())
.map(|mb| mb * 1024 * 1024)
.unwrap_or(DEFAULT_MAX_DECOMPRESSED_BYTES)
}
fn effective_limit_from_env_pair(new_val: Option<&str>, old_val: Option<&str>) -> u64 {
effective_limit_from_str(new_val.or(old_val))
}
pub(crate) fn effective_limit() -> u64 {
let new_val = std::env::var("XBERG_NATIVE_PDF_MAX_DECOMPRESS_MB").ok();
let previous_val = std::env::var("XBERG_PDF_OXIDE_MAX_DECOMPRESS_MB").ok();
let old_val = std::env::var("PDF_OXIDE_MAX_DECOMPRESS_MB").ok();
effective_limit_from_env_pair(new_val.or(previous_val).as_deref(), old_val.as_deref())
}
fn looks_like_real_stream(output: &[u8]) -> bool {
if output.is_empty() {
return false;
}
const MARKERS: &[&[u8]] = &[b"BT", b"ET", b"Tj", b"TJ", b"Tm", b"Td", b"stream", b"endobj", b"%PDF-"];
for m in MARKERS {
if output.windows(m.len()).any(|w| w == *m) {
return true;
}
}
let printable = output
.iter()
.filter(|&&b| (0x20..=0x7E).contains(&b) || b == b'\t' || b == b'\n' || b == b'\r')
.count();
printable * 5 >= output.len() * 4
}
#[inline]
fn check_limit(output: &[u8], limit: u64) -> Result<()> {
if output.len() as u64 >= limit {
return Err(Error::Decode(format!(
"FlateDecode output reached the {} MB safety limit; \
stream may be a flate bomb or an unusually large image",
limit / (1024 * 1024)
)));
}
Ok(())
}
fn accept_as_partial(output: &[u8], limit: u64) -> Result<bool> {
if output.is_empty() || !looks_like_real_stream(output) {
return Ok(false);
}
check_limit(output, limit)?;
Ok(true)
}
enum PrimaryAttempt {
Recovered(Vec<u8>),
Failed(std::io::Error),
}
fn try_zlib_decode(input: &[u8], limit: u64) -> Result<PrimaryAttempt> {
let mut decoder = ZlibDecoder::new(input).take(limit);
let mut output = Vec::new();
match decoder.read_to_end(&mut output) {
Ok(_) => {
check_limit(&output, limit)?;
Ok(PrimaryAttempt::Recovered(output))
}
Err(e) => {
if accept_as_partial(&output, limit)? {
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
error = %e,
"partial recovery: extracted bytes before corruption"
);
return Ok(PrimaryAttempt::Recovered(output));
}
Ok(PrimaryAttempt::Failed(e))
}
}
}
fn try_raw_deflate(input: &[u8], limit: u64) -> Result<PrimaryAttempt> {
tracing::debug!(filter = "FlateDecode", "zlib decode failed, trying raw deflate");
let mut deflate_decoder = DeflateDecoder::new(input).take(limit);
let mut output = Vec::new();
match deflate_decoder.read_to_end(&mut output) {
Ok(_) => {
check_limit(&output, limit)?;
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"recovered via raw deflate fallback after corrupt zlib header"
);
Ok(PrimaryAttempt::Recovered(output))
}
Err(deflate_err) => {
if accept_as_partial(&output, limit)? {
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"raw deflate partial recovery: extracted bytes before error"
);
return Ok(PrimaryAttempt::Recovered(output));
}
Ok(PrimaryAttempt::Failed(deflate_err))
}
}
}
pub struct FlateDecoder {
pub max_decompressed_bytes: u64,
}
impl Default for FlateDecoder {
fn default() -> Self {
Self {
max_decompressed_bytes: effective_limit(),
}
}
}
impl FlateDecoder {
pub fn with_limit(limit: u64) -> Self {
Self {
max_decompressed_bytes: limit,
}
}
}
impl StreamDecoder for FlateDecoder {
fn decode(&self, input: &[u8]) -> Result<Vec<u8>> {
let limit = self.max_decompressed_bytes;
let zlib_err = match try_zlib_decode(input, limit)? {
PrimaryAttempt::Recovered(output) => return Ok(output),
PrimaryAttempt::Failed(e) => e,
};
let deflate_err = match try_raw_deflate(input, limit)? {
PrimaryAttempt::Recovered(output) => return Ok(output),
PrimaryAttempt::Failed(e) => e,
};
if let Some(output) = try_header_skip_deflate(input, limit)? {
return Ok(output);
}
if let Some(output) = try_corrected_header(input, limit)? {
return Ok(output);
}
if let Some(output) = try_brute_force_scan(input, limit)? {
return Ok(output);
}
Err(Error::Decode(format!(
"FlateDecode decompression failed: stream is labeled as compressed but all decompression attempts failed. \
This violates PDF Spec ISO 32000-1:2008, Section 7.3.8.2. \
Zlib error: {}, Deflate error: {}. Compressed size: {} bytes.",
zlib_err,
deflate_err,
input.len()
)))
}
fn name(&self) -> &str {
"FlateDecode"
}
}
fn try_header_skip_deflate(input: &[u8], limit: u64) -> Result<Option<Vec<u8>>> {
if input.len() <= 2 {
return Ok(None);
}
tracing::debug!(
filter = "FlateDecode",
"trying deflate after skipping potential corrupt zlib header"
);
let mut output = Vec::new();
let mut deflate_decoder = DeflateDecoder::new(&input[2..]).take(limit);
match deflate_decoder.read_to_end(&mut output) {
Ok(_) => {
check_limit(&output, limit)?;
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"recovered by skipping corrupt zlib header bytes"
);
Ok(Some(output))
}
Err(_) => {
if accept_as_partial(&output, limit)? {
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"header-skip partial recovery"
);
return Ok(Some(output));
}
Ok(None)
}
}
}
fn try_corrected_header(input: &[u8], limit: u64) -> Result<Option<Vec<u8>>> {
if input.len() < 2 {
return Ok(None);
}
let first_byte = input[0];
let compression_method = first_byte & 0x0F;
if compression_method == 8 {
return Ok(None);
}
tracing::debug!(
filter = "FlateDecode",
compression_method,
header_byte = format_args!("0x{:02x}", first_byte),
"invalid compression method in header byte, trying corrected header"
);
let mut corrected = input.to_vec();
corrected[0] = (first_byte & 0xF0) | 0x08;
let mut output = Vec::new();
let mut decoder = ZlibDecoder::new(&corrected[..]).take(limit);
let read_ok = decoder.read_to_end(&mut output).is_ok();
if read_ok && !output.is_empty() {
check_limit(&output, limit)?;
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"recovered via corrected zlib header byte"
);
return Ok(Some(output));
}
if accept_as_partial(&output, limit)? {
tracing::warn!(
filter = "FlateDecode",
bytes = output.len(),
"header-correction partial recovery"
);
return Ok(Some(output));
}
tracing::debug!(filter = "FlateDecode", "header correction failed");
Ok(None)
}
fn try_brute_force_scan(input: &[u8], limit: u64) -> Result<Option<Vec<u8>>> {
tracing::debug!(filter = "FlateDecode", "trying brute-force scan for valid deflate data");
let max_offset = std::cmp::min(20, input.len());
for offset in 0..max_offset {
if offset == 0 || offset == 2 {
continue;
}
if let Some(output) = try_offset_deflate(input, offset, limit)? {
return Ok(Some(output));
}
}
Ok(None)
}
fn try_offset_deflate(input: &[u8], offset: usize, limit: u64) -> Result<Option<Vec<u8>>> {
let mut output = Vec::new();
let mut deflate_decoder = DeflateDecoder::new(&input[offset..]).take(limit);
let read_ok = deflate_decoder.read_to_end(&mut output).is_ok();
if output.is_empty() {
return Ok(None);
}
check_limit(&output, limit)?;
let decoded_str = String::from_utf8_lossy(&output);
let has_pdf_operators = decoded_str.contains("BT")
|| decoded_str.contains("ET")
|| decoded_str.contains("Tj")
|| decoded_str.contains("TJ")
|| decoded_str.contains("Tm")
|| decoded_str.contains("Td");
if !has_pdf_operators {
if read_ok {
tracing::trace!(
filter = "FlateDecode",
offset,
bytes = output.len(),
"brute-force offset produced no valid PDF operators, trying next offset"
);
} else {
tracing::trace!(
filter = "FlateDecode",
offset,
"brute-force partial recovery has no valid PDF operators, trying next offset"
);
}
return Ok(None);
}
if read_ok {
tracing::warn!(
filter = "FlateDecode",
offset,
bytes = output.len(),
"brute-force deflate recovery succeeded (validated PDF content)"
);
} else {
tracing::warn!(
filter = "FlateDecode",
offset,
bytes = output.len(),
"brute-force partial recovery (validated PDF content)"
);
}
Ok(Some(output))
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
#[test]
fn looks_like_real_stream_rejects_repeating_garbage() {
let garbage =
b"P\xffj!}\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd\xef\xbd\xbd"
.repeat(4);
assert!(
!looks_like_real_stream(&garbage),
"misaligned-deflate garbage must be rejected as a partial recovery"
);
}
#[test]
fn looks_like_real_stream_accepts_content_stream_operators() {
let real = b"BT /F1 12 Tf 100 700 Td (hello) Tj ET";
assert!(looks_like_real_stream(real));
}
#[test]
fn looks_like_real_stream_accepts_ascii_only_object_stream() {
let object_stream = b"1 0 obj\n<< /Length 42 >>\nstream\nhello world\nendstream\nendobj\n";
assert!(looks_like_real_stream(object_stream));
}
#[test]
fn looks_like_real_stream_rejects_empty() {
assert!(!looks_like_real_stream(&[]));
}
#[test]
fn test_flate_decode_simple() {
let decoder = FlateDecoder::default();
let original = b"Hello, FlateDecode!";
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(original).unwrap();
let compressed = encoder.finish().unwrap();
let decoded = decoder.decode(&compressed).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_flate_decode_empty() {
let decoder = FlateDecoder::default();
let original = b"";
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(original).unwrap();
let compressed = encoder.finish().unwrap();
let decoded = decoder.decode(&compressed).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_flate_decode_large_data() {
let decoder = FlateDecoder::default();
let original = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ".repeat(1000);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&original).unwrap();
let compressed = encoder.finish().unwrap();
let decoded = decoder.decode(&compressed).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_flate_decode_invalid_data() {
let decoder = FlateDecoder::default();
let invalid = b"This is not zlib compressed data";
let result = decoder.decode(invalid);
assert!(result.is_err());
if let Err(e) = result {
let error_msg = format!("{}", e);
assert!(error_msg.contains("FlateDecode decompression failed"));
}
}
#[test]
fn test_flate_decoder_name() {
let decoder = FlateDecoder::default();
assert_eq!(decoder.name(), "FlateDecode");
}
#[test]
fn test_flate_bomb_rejected() {
let large = vec![0u8; DEFAULT_MAX_DECOMPRESSED_BYTES as usize];
let result = check_limit(&large, DEFAULT_MAX_DECOMPRESSED_BYTES);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("safety limit"));
}
#[test]
fn test_check_limit_below_threshold() {
let small = vec![0u8; 1024];
assert!(check_limit(&small, DEFAULT_MAX_DECOMPRESSED_BYTES).is_ok());
}
#[test]
fn test_custom_limit_accepts_data_within_limit() {
let original = b"x".repeat(512);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&original).unwrap();
let compressed = encoder.finish().unwrap();
let decoder = FlateDecoder::with_limit(1024);
let decoded = decoder.decode(&compressed).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_custom_limit_rejects_data_over_limit() {
let original = b"x".repeat(100);
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&original).unwrap();
let compressed = encoder.finish().unwrap();
let decoder = FlateDecoder::with_limit(10);
let result = decoder.decode(&compressed);
assert!(result.is_err(), "expected rejection when output exceeds custom limit");
}
#[test]
fn test_bomb_error_does_not_expose_internal_symbol_name() {
let large = vec![0u8; DEFAULT_MAX_DECOMPRESSED_BYTES as usize];
let result = check_limit(&large, DEFAULT_MAX_DECOMPRESSED_BYTES);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
!msg.contains("MAX_DECOMPRESSED_BYTES"),
"error message must not reference internal symbol names: {msg}"
);
}
#[test]
fn test_effective_limit_env_variable() {
assert_eq!(effective_limit_from_str(None), DEFAULT_MAX_DECOMPRESSED_BYTES);
assert_eq!(effective_limit_from_str(Some("64")), 64 * 1024 * 1024);
assert_eq!(
effective_limit_from_str(Some("not_a_number")),
DEFAULT_MAX_DECOMPRESSED_BYTES
);
}
#[test]
fn test_effective_limit_prefers_new_env_var_and_falls_back_to_old() {
assert_eq!(effective_limit_from_env_pair(Some("64"), Some("128")), 64 * 1024 * 1024);
assert_eq!(effective_limit_from_env_pair(None, Some("64")), 64 * 1024 * 1024);
assert_eq!(
effective_limit_from_env_pair(None, None),
DEFAULT_MAX_DECOMPRESSED_BYTES
);
}
}