Files
pdfcraft/vendor/lopdf/src/common_data_structures/mod.rs
T
Brandon ThomasandClaude Opus 5.5 1449b71a19 Never crash: bound the allocations behind the nightly fuzz runner's out-of-memory deaths
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner
running two of them was killed every night). Three causes, each with a test:
- lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before
  reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf
  0.45.0 refuses rows longer than the data (and uses saturating row widths);
  its load-time stream decoding also gets a 256 MiB limit (it had none).
- hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only
  by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps
  the cell within 3000 px a side, lowering the resolution instead.
- cos decoded cross-reference streams up to the general 1 GiB limit; they now
  stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the
  section is reconstructed (logged) instead.

On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB,
and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:59:25 -04:00

107 lines
3.7 KiB
Rust

use crate::{
Error, Object, Result, StringFormat,
encodings::{self, bytes_to_string},
};
/// Creates a text string.
/// If the input only contains ASCII characters, the string is encoded
/// in PDFDocEncoding, otherwise in UTF-16BE.
pub fn text_string(text: &str) -> Object {
if text.is_ascii() {
return Object::String(text.into(), StringFormat::Literal);
}
let mut string = Vec::new();
encodings::encode_utf16_be(text, &mut string);
Object::String(string, StringFormat::Hexadecimal)
}
/// Decodes a text string.
/// Depending on the BOM at the start of the string, a different encoding is chosen.
/// All encodings specified in PDF2.0 are supported (PDFDocEncoding, UTF-16BE,
/// and UTF-8).
pub fn decode_text_string(obj: &Object) -> Result<String> {
let s = obj.as_str()?;
if s.starts_with(b"\xFE\xFF") {
// Detected UTF-16BE BOM
String::from_utf16(
&s[2..]
.chunks(2)
.map(|c| {
if c.len() == 1 {
u16::from_be_bytes([c[0], 0])
} else {
u16::from_be_bytes(c.try_into().unwrap())
}
})
.collect::<Vec<u16>>(),
)
.map_err(|_| Error::TextStringDecode)
} else if s.starts_with(b"\xEF\xBB\xBF") {
// Detected UTF-8 BOM
String::from_utf8(s.to_vec()).map_err(|_| Error::TextStringDecode)
} else {
// If neither BOM is detected, PDFDocEncoding is used
let mut out = String::new();
bytes_to_string(&encodings::PDF_DOC_ENCODING, s, &mut out)?;
Ok(out)
}
}
#[cfg(test)]
mod test {
use crate::{
Object, StringFormat, common_data_structures::decode_text_string, encodings, text_string, writer::Writer,
};
#[test]
fn spec_example1_encode() {
let input = "text‰";
let text_string = encodings::string_to_bytes(&encodings::PDF_DOC_ENCODING, input);
// let text_string = input.bytes().collect::<Vec<_>>();
let dict = Object::Dictionary(dictionary!(
"Key" => Object::String(text_string, StringFormat::Literal),
));
let mut actual = vec![];
Writer::write_object(&mut actual, &dict).unwrap();
// "\x8B" is equivalent to the escaped version "\\213" which is used
// in the original example.
let expected = b"<</Key(text\x8B)>>";
assert_eq!(actual.as_slice(), expected);
}
#[test]
fn spec_example1_decode() {
let input = b"<</Key(text\\213)>>";
let dict = crate::parser::direct_object(input).unwrap();
let dict = dict.as_dict().unwrap();
let actual = decode_text_string(dict.get(b"Key").unwrap()).unwrap();
let expected = "text‰";
assert_eq!(&actual, expected);
}
#[test]
fn spec_example2_encode() {
// Russian for "test"
let input = "тест";
// let text_string = encodings::string_to_bytes(encodings::PDF_DOC_ENCODING, input);
let dict = Object::Dictionary(dictionary!(
"Key" => text_string(input),
));
let mut actual = vec![];
Writer::write_object(&mut actual, &dict).unwrap();
let expected = b"<</Key<FEFF0442043504410442>>>";
assert_eq!(actual.as_slice(), expected);
}
#[test]
fn spec_example2_decode() {
let input = b"<</Key<FEFF0442043504410442>>>";
let dict = crate::parser::direct_object(input).unwrap();
let dict = dict.as_dict().unwrap();
let actual = decode_text_string(dict.get(b"Key").unwrap()).unwrap();
// Russian for "test"
let expected = "тест";
assert_eq!(&actual, expected);
}
}