use stet_pdf_reader::PdfDocument;
fn build_pdf(objs: &[(u32, Vec<u8>)]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = std::collections::BTreeMap::new();
for (num, body) in objs {
offsets.insert(*num, out.len());
out.extend_from_slice(format!("{num} 0 obj\n").as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
let max = offsets.keys().copied().max().unwrap_or(0) + 1;
out.extend_from_slice(format!("xref\n0 {max}\n0000000000 65535 f \n").as_bytes());
for i in 1..max {
let off = offsets.get(&i).copied().unwrap_or(0);
out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(
format!("trailer\n<</Size {max}/Root 1 0 R>>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
);
out
}
fn stream_obj(body: &[u8]) -> Vec<u8> {
let mut v = format!("<</Length {}>>\nstream\n", body.len()).into_bytes();
v.extend_from_slice(body);
v.extend_from_slice(b"endstream");
v
}
fn one_page_doc(page_extra: &[u8], contents: &[u8], extra: &[(u32, Vec<u8>)]) -> Vec<u8> {
let mut objs: Vec<(u32, Vec<u8>)> = vec![
(1, b"<</Type/Catalog/Pages 2 0 R>>".to_vec()),
(2, b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec()),
(3, {
let mut d = b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 100 100]/Contents 4 0 R".to_vec();
d.extend_from_slice(page_extra);
d.extend_from_slice(b">>");
d
}),
(4, stream_obj(contents)),
];
objs.extend_from_slice(extra);
build_pdf(&objs)
}
fn load_and_render(data: &[u8]) {
if let Ok(doc) = PdfDocument::from_bytes(data) {
for page in 0..doc.page_count() {
let _ = doc.render_page(page, 72.0);
}
}
}
#[test]
fn deeply_nested_arrays_do_not_overflow_the_stack() {
const DEPTH: usize = 200_000;
let mut junk = b"[".repeat(DEPTH);
junk.extend_from_slice(&b"]".repeat(DEPTH));
let mut catalog = b"<</Type/Catalog/Pages 2 0 R/Junk ".to_vec();
catalog.extend_from_slice(&junk);
catalog.extend_from_slice(b">>");
load_and_render(&build_pdf(&[
(1, catalog),
(2, b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec()),
(
3,
b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 100 100]>>".to_vec(),
),
]));
}
#[test]
fn deeply_nested_dictionaries_do_not_overflow_the_stack() {
const DEPTH: usize = 200_000;
let mut junk = b"<</A ".repeat(DEPTH);
junk.extend_from_slice(b"0");
junk.extend_from_slice(&b">>".repeat(DEPTH));
let mut catalog = b"<</Type/Catalog/Pages 2 0 R/Junk ".to_vec();
catalog.extend_from_slice(&junk);
catalog.extend_from_slice(b">>");
load_and_render(&build_pdf(&[
(1, catalog),
(2, b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec()),
(
3,
b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 100 100]>>".to_vec(),
),
]));
}
#[test]
fn deeply_nested_content_stream_array_does_not_overflow_the_stack() {
const DEPTH: usize = 200_000;
let mut cs = b"[".repeat(DEPTH);
cs.extend_from_slice(&b"]".repeat(DEPTH));
cs.extend_from_slice(b" TJ\n");
load_and_render(&one_page_doc(b"", &cs, &[]));
}
#[test]
fn deeply_nested_calculator_function_does_not_overflow_the_stack() {
const DEPTH: usize = 200_000;
let mut code = b"{".to_vec();
code.extend_from_slice(&b"{".repeat(DEPTH));
code.extend_from_slice(&b"}".repeat(DEPTH));
code.extend_from_slice(b" }");
load_and_render(&one_page_doc(
b"/Resources<</Shading<</S1 5 0 R>>>>",
b"/S1 sh\n",
&[
(
5,
b"<</ShadingType 2/ColorSpace/DeviceGray/Coords[0 0 100 100]/Function 6 0 R>>"
.to_vec(),
),
(6, {
let mut v = format!(
"<</FunctionType 4/Domain[0 1]/Range[0 1]/Length {}>>\nstream\n",
code.len()
)
.into_bytes();
v.extend_from_slice(&code);
v.extend_from_slice(b"\nendstream");
v
}),
],
));
}
fn stitching_fn(functions: &[u8]) -> Vec<u8> {
let mut v = b"<</FunctionType 3/Domain[0 1]/Range[0 1]/Functions".to_vec();
v.extend_from_slice(functions);
v.extend_from_slice(b"/Bounds[]/Encode[0 1]>>");
v
}
fn shading_page(extra: Vec<(u32, Vec<u8>)>) -> Vec<u8> {
let mut objs = vec![(
5,
b"<</ShadingType 2/ColorSpace/DeviceGray/Coords[0 0 100 100]/Function 6 0 R>>".to_vec(),
)];
objs.extend(extra);
one_page_doc(b"/Resources<</Shading<</S1 5 0 R>>>>", b"/S1 sh\n", &objs)
}
#[test]
fn self_referential_stitching_function_does_not_overflow_the_stack() {
load_and_render(&shading_page(vec![(6, stitching_fn(b"[6 0 R]"))]));
}
#[test]
fn stitching_function_ring_does_not_overflow_the_stack() {
load_and_render(&shading_page(vec![
(6, stitching_fn(b"[7 0 R]")),
(7, stitching_fn(b"[6 0 R]")),
]));
}
#[test]
fn stitching_function_may_reference_one_child_twice() {
let data = shading_page(vec![
(
6,
b"<</FunctionType 3/Domain[0 1]/Range[0 1]/Functions[7 0 R 7 0 R]\
/Bounds[0.5]/Encode[0 1 0 1]>>"
.to_vec(),
),
(
7,
b"<</FunctionType 2/Domain[0 1]/C0[0]/C1[1]/N 1>>".to_vec(),
),
]);
let doc = PdfDocument::from_bytes(&data).expect("document should parse");
let list = doc.render_page(0, 72.0).expect("page should render");
assert!(
!list.is_empty(),
"a stitching function naming the same child twice is legal and must still draw"
);
}
#[test]
fn self_referential_type3_glyph_does_not_overflow_the_stack() {
let glyph = b"10 0 0 0 10 10 d1\nBT /F1 1 Tf (A) Tj ET\n";
load_and_render(&one_page_doc(
b"/Resources<</Font<</F1 5 0 R>>>>",
b"BT /F1 12 Tf 10 50 Td (A) Tj ET\n",
&[
(
5,
b"<</Type/Font/Subtype/Type3/FontBBox[0 0 10 10]\
/FontMatrix[0.001 0 0 0.001 0 0]/CharProcs<</A 6 0 R>>\
/Encoding<</Type/Encoding/Differences[65/A]>>/FirstChar 65/LastChar 65\
/Widths[10]/Resources<</Font<</F1 5 0 R>>>>>>"
.to_vec(),
),
(6, stream_obj(glyph)),
],
));
}
#[test]
fn type3_glyph_ring_does_not_overflow_the_stack() {
let t3 = |charproc: u32| -> Vec<u8> {
format!(
"<</Type/Font/Subtype/Type3/FontBBox[0 0 10 10]\
/FontMatrix[0.001 0 0 0.001 0 0]/CharProcs<</A {charproc} 0 R>>\
/Encoding<</Type/Encoding/Differences[65/A]>>/FirstChar 65/LastChar 65\
/Widths[10]/Resources<</Font<</F1 5 0 R/F2 7 0 R>>>>>>"
)
.into_bytes()
};
load_and_render(&one_page_doc(
b"/Resources<</Font<</F1 5 0 R/F2 7 0 R>>>>",
b"BT /F1 12 Tf 10 50 Td (A) Tj ET\n",
&[
(5, t3(6)),
(6, stream_obj(b"10 0 0 0 10 10 d1\nBT /F2 1 Tf (A) Tj ET\n")),
(7, t3(8)),
(8, stream_obj(b"10 0 0 0 10 10 d1\nBT /F1 1 Tf (A) Tj ET\n")),
],
));
}
#[test]
fn self_referential_soft_mask_does_not_overflow_the_stack() {
load_and_render(&one_page_doc(
b"/Resources<</ExtGState<</G1 5 0 R>>/XObject<</X1 7 0 R>>>>",
b"/G1 gs 0 0 100 100 re f\n",
&[
(
5,
b"<</Type/ExtGState/SMask<</S/Luminosity/G 7 0 R>>>>".to_vec(),
),
(7, {
let body = b"/G1 gs 0 0 100 100 re f\n";
let mut v = format!(
"<</Type/XObject/Subtype/Form/BBox[0 0 100 100]\
/Group<</S/Transparency/CS/DeviceGray>>\
/Resources<</ExtGState<</G1 5 0 R>>>>/Length {}>>\nstream\n",
body.len()
)
.into_bytes();
v.extend_from_slice(body);
v.extend_from_slice(b"endstream");
v
}),
],
));
}
fn renders_an_image(data: &[u8]) -> bool {
use stet_graphics::display_list::DisplayElement;
let Ok(doc) = PdfDocument::from_bytes(data) else {
return false;
};
let Ok(list) = doc.render_page(0, 72.0) else {
return false;
};
list.elements()
.iter()
.any(|e| matches!(e, DisplayElement::Image { .. }))
}
fn image_doc(width: &str, height: &str, bpc: &str, cs: &[u8]) -> Vec<u8> {
let payload: &[u8] = &[
0x78, 0x9c, 0x63, 0x60, 0x18, 0x05, 0xa3, 0x60, 0x14, 0x8c, 0x02, 0x08, 0x00, 0x00, 0x04,
0x00, 0x00, 0x01,
];
let mut xobj = format!(
"<</Type/XObject/Subtype/Image/Width {width}/Height {height}\
/BitsPerComponent {bpc}/ColorSpace "
)
.into_bytes();
xobj.extend_from_slice(cs);
xobj.extend_from_slice(
format!("/Filter/FlateDecode/Length {}>>\nstream\n", payload.len()).as_bytes(),
);
xobj.extend_from_slice(payload);
xobj.extend_from_slice(b"\nendstream");
one_page_doc(
b"/Resources<</XObject<</Im1 5 0 R>>>>",
b"q 100 0 0 100 0 0 cm /Im1 Do Q\n",
&[(5, xobj)],
)
}
#[test]
fn image_dimensions_that_overflow_u32_are_rejected() {
assert!(
!renders_an_image(&image_doc("65537", "65536", "8", b"/DeviceGray")),
"65537 x 65536 overflows u32 to 65536; the image must be dropped"
);
}
#[test]
fn image_dimensions_that_wrap_to_zero_are_rejected() {
assert!(
!renders_an_image(&image_doc("65536", "65536", "8", b"/DeviceGray")),
"65536 x 65536 overflows u32 to 0; the image must be dropped"
);
}
#[test]
fn oversized_indexed_image_is_rejected() {
assert!(
!renders_an_image(&image_doc(
"65537",
"65536",
"8",
b"[/Indexed /DeviceRGB 1 <000000FFFFFF>]",
)),
"the Indexed expansion path must not receive an overflowing size"
);
}
#[test]
fn oversized_low_bpc_image_is_rejected() {
assert!(
!renders_an_image(&image_doc("65537", "65536", "4", b"/DeviceGray")),
"the sub-8-bpc expansion path must not receive an overflowing size"
);
}
#[test]
fn image_dimension_beyond_u32_is_rejected_not_truncated() {
assert!(
!renders_an_image(&image_doc("4294967297", "4294967297", "8", b"/DeviceGray")),
"4294967297 must be rejected, not truncated to a 1-pixel image"
);
}
#[test]
fn out_of_range_bits_per_component_is_rejected() {
assert!(
!renders_an_image(&image_doc("8", "8", "99", b"/DeviceGray")),
"/BitsPerComponent 99 reaches `1u32 << bpc` and must be rejected"
);
}
#[test]
fn non_positive_image_dimensions_are_rejected() {
assert!(!renders_an_image(&image_doc(
"-1",
"-1",
"8",
b"/DeviceGray"
)));
assert!(!renders_an_image(&image_doc("0", "0", "8", b"/DeviceGray")));
}
#[test]
fn prepress_scale_image_dimensions_are_accepted() {
for (label, w, h) in [
("40x28in press sheet @ 600dpi", 24_000, 16_800),
("A0 poster @ 600dpi", 19_800, 28_200),
("60x40in grand format @ 600dpi", 36_000, 24_000),
] {
let data = image_doc(&w.to_string(), &h.to_string(), "8", b"/DeviceGray");
assert!(
PdfDocument::from_bytes(&data).is_ok(),
"{label} ({w}x{h}) must not be rejected by the image size bounds"
);
}
}
#[test]
fn corpus_scale_image_dimensions_are_accepted() {
let data = image_doc("34862", "4332", "8", b"/DeviceGray");
assert!(
renders_an_image(&data),
"a 34862x4332 image is within real-world range and must still draw"
);
}
fn predictor_doc(parms: &[u8]) -> Vec<u8> {
let payload: &[u8] = &[
0x78, 0x9c, 0x63, 0x60, 0x18, 0x05, 0xa3, 0x60, 0x14, 0x8c, 0x02, 0x08, 0x00, 0x00, 0x04,
0x00, 0x00, 0x01,
];
let mut xobj = b"<</Type/XObject/Subtype/Image/Width 16/Height 16/BitsPerComponent 8\
/ColorSpace/DeviceGray/Filter/FlateDecode/DecodeParms"
.to_vec();
xobj.extend_from_slice(parms);
xobj.extend_from_slice(format!("/Length {}>>\nstream\n", payload.len()).as_bytes());
xobj.extend_from_slice(payload);
xobj.extend_from_slice(b"\nendstream");
one_page_doc(
b"/Resources<</XObject<</Im1 5 0 R>>>>",
b"q 100 0 0 100 0 0 cm /Im1 Do Q\n",
&[(5, xobj)],
)
}
#[test]
fn zero_predictor_parameters_do_not_panic() {
for parms in [
&b"<</Predictor 2/Columns 0/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 8/Colors 0/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 8/Colors 1/BitsPerComponent 0>>"[..],
&b"<</Predictor 12/Columns 0/Colors 1/BitsPerComponent 8>>"[..],
] {
load_and_render(&predictor_doc(parms));
}
}
#[test]
fn negative_predictor_parameters_do_not_allocate_wildly() {
for parms in [
&b"<</Predictor 2/Columns -1/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 8/Colors -1/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 8/Colors 1/BitsPerComponent -1>>"[..],
&b"<</Predictor 15/Columns -1/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 15/Columns 8/Colors -1/BitsPerComponent 8>>"[..],
] {
load_and_render(&predictor_doc(parms));
}
}
#[test]
fn overflowing_predictor_parameters_do_not_panic() {
for parms in [
&b"<</Predictor 2/Columns 4294967296/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 15/Columns 9223372036854775807/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 9223372036854775807/Colors 9223372036854775807\
/BitsPerComponent 8>>"[..],
] {
load_and_render(&predictor_doc(parms));
}
}
#[test]
fn ordinary_predictor_parameters_still_apply() {
for parms in [
&b"<</Predictor 12/Columns 16/Colors 1/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 16/Colors 3/BitsPerComponent 8>>"[..],
&b"<</Predictor 15/Columns 16/Colors 4/BitsPerComponent 8>>"[..],
&b"<</Predictor 2/Columns 16/Colors 1/BitsPerComponent 4>>"[..],
] {
assert!(
renders_an_image(&predictor_doc(parms)),
"a well-formed /DecodeParms must still decode: {}",
String::from_utf8_lossy(parms)
);
}
}
fn ps_cidfont_doc(header: &[u8]) -> Vec<u8> {
let mut font = b"%!PS-Adobe-3.0 Resource-CIDFont\n".to_vec();
font.extend_from_slice(header);
font.extend_from_slice(b"\n(Binary) 64 StartData\n");
font.extend_from_slice(&[0u8; 64]);
let mut fontfile =
format!("<</Subtype/CIDFontType0C/Length {}>>\nstream\n", font.len()).into_bytes();
fontfile.extend_from_slice(&font);
fontfile.extend_from_slice(b"\nendstream");
one_page_doc(
b"/Resources<</Font<</F1 5 0 R>>>>",
b"BT /F1 12 Tf 10 50 Td <0001> Tj ET\n",
&[
(
5,
b"<</Type/Font/Subtype/Type0/BaseFont/Test/Encoding/Identity-H\
/DescendantFonts[6 0 R]>>"
.to_vec(),
),
(
6,
b"<</Type/Font/Subtype/CIDFontType0/BaseFont/Test/CIDSystemInfo\
<</Registry(Adobe)/Ordering(Identity)/Supplement 0>>\
/FontDescriptor 7 0 R/DW 1000>>"
.to_vec(),
),
(
7,
b"<</Type/FontDescriptor/FontName/Test/Flags 4/ItalicAngle 0/Ascent 800\
/Descent -200/CapHeight 700/StemV 80/FontBBox[0 0 1000 1000]\
/FontFile3 8 0 R>>"
.to_vec(),
),
(8, fontfile),
],
)
}
#[test]
fn ps_cidfont_zero_entry_size_does_not_allocate_wildly() {
load_and_render(&ps_cidfont_doc(
b"/CIDCount 1000000000000 /FDBytes 0 /GDBytes 0 /SDBytes 4 /SubrCount 0 \
/SubrMapOffset 0",
));
}
#[test]
fn ps_cidfont_oversized_subr_count_does_not_allocate_wildly() {
load_and_render(&ps_cidfont_doc(
b"/CIDCount 1 /FDBytes 0 /GDBytes 1 /SDBytes 0 \
/SubrCount 18446744073709551615 /SubrMapOffset 0",
));
load_and_render(&ps_cidfont_doc(
b"/CIDCount 1 /FDBytes 0 /GDBytes 1 /SDBytes 18446744073709551615 \
/SubrCount 18446744073709551615 /SubrMapOffset 18446744073709551615",
));
}
#[test]
fn ps_cidfont_overflowing_counts_do_not_panic() {
load_and_render(&ps_cidfont_doc(
b"/CIDCount 18446744073709551615 /FDBytes 0 /GDBytes 0 /SDBytes 4 \
/SubrCount 0 /SubrMapOffset 0",
));
load_and_render(&ps_cidfont_doc(
b"/CIDCount 2 /FDBytes 18446744073709551615 /GDBytes 18446744073709551615 \
/SDBytes 4 /SubrCount 0 /SubrMapOffset 0",
));
load_and_render(&ps_cidfont_doc(
b"/CIDCount 9223372036854775807 /FDBytes 1 /GDBytes 1 /SDBytes 4 \
/SubrCount 0 /SubrMapOffset 0",
));
}