mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
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>
107 lines
3.7 KiB
Rust
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);
|
|
}
|
|
}
|