use super::outline::{ObjectAllocator, ObjectSink};
use super::types::PdfRef;
use super::xref::{Trailer, dense_xref_len, write_xref};
use crate::fallible::{len_u64, reserve_exact};
use crate::{Cancellation, Error, Limits, Result, write_counted};
use std::io::Write;
use std::mem::size_of;
pub const MAX_CLASSIC_PDF_BYTES: u64 = 9_999_999_999;
pub const MAX_PDF_INTEGER: u64 = i32::MAX as u64;
pub const MAX_PDF_OBJECTS: u32 = 8_388_607;
pub(super) const HEADER: &[u8] = b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ObjectId(u32);
impl ObjectId {
pub const fn number(self) -> u32 {
self.0
}
}
impl From<ObjectId> for PdfRef {
fn from(id: ObjectId) -> Self {
Self {
number: id.0,
generation: 0,
}
}
}
pub(super) struct Output<'a, W: Write, C: Cancellation> {
sink: &'a mut W,
pub(super) limits: &'a Limits,
pub(super) cancellation: &'a C,
pub(super) position: u64,
failed: bool,
}
impl<'a, W: Write, C: Cancellation> Output<'a, W, C> {
pub(super) fn new(sink: &'a mut W, limits: &'a Limits, cancellation: &'a C) -> Self {
Self {
sink,
limits,
cancellation,
position: 0,
failed: false,
}
}
pub(super) fn ensure_healthy(&self) -> Result<()> {
if self.failed {
Err(Error::invalid(
"PDF writer cannot continue after a sink failure",
))
} else {
Ok(())
}
}
pub(super) fn write(&mut self, bytes: &[u8]) -> Result<()> {
self.ensure_healthy()?;
let attempted = self
.position
.checked_add(len_u64(bytes.len()))
.ok_or(Error::invalid("PDF output byte count overflows 64 bits"))?;
check_classic_pdf_bytes(attempted)?;
self.write_unbounded(bytes)
}
pub(super) fn write_unbounded(&mut self, bytes: &[u8]) -> Result<()> {
self.ensure_healthy()?;
let result = write_counted(
self.sink,
bytes,
&mut self.position,
self.limits,
self.cancellation,
);
if result.is_err() {
self.failed = true;
}
result
}
pub(super) fn flush(&mut self) -> Result<()> {
self.ensure_healthy()?;
if self.cancellation.is_cancelled() {
return Err(crate::ErrorKind::Cancelled.into());
}
self.sink.flush().inspect_err(|_| {
self.failed = true;
})?;
if self.cancellation.is_cancelled() {
return Err(crate::ErrorKind::Cancelled.into());
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
Idle,
Object,
Stream {
length_id: ObjectId,
data_start: u64,
},
}
pub struct PdfWriter<'a, W: Write, C: Cancellation> {
out: Output<'a, W, C>,
offsets: Vec<u64>,
state: State,
}
impl<'a, W: Write, C: Cancellation> PdfWriter<'a, W, C> {
pub fn new(sink: &'a mut W, limits: &'a Limits, cancellation: &'a C) -> Result<Self> {
limits.validate()?;
let mut out = Output::new(sink, limits, cancellation);
out.write(HEADER)?;
Ok(Self {
out,
offsets: Vec::new(),
state: State::Idle,
})
}
pub const fn position(&self) -> u64 {
self.out.position
}
#[cfg(test)]
pub(crate) fn set_position_for_test(&mut self, position: u64) {
self.out.position = position;
}
pub fn reserve_object(&mut self) -> Result<ObjectId> {
self.prepare_objects(1)?;
let number = checked_object_number(self.offsets.len() + 1)?;
self.offsets.push(0);
Ok(ObjectId(number))
}
pub(crate) fn prepare_objects(&mut self, additional_objects: usize) -> Result<()> {
self.out.ensure_healthy()?;
let limits = self.out.limits;
let next_count = self
.offsets
.len()
.checked_add(additional_objects)
.ok_or(Error::invalid("PDF object count overflows address space"))?;
checked_object_number(next_count)?;
let max_slots =
(limits.max_allocation_bytes / size_of::<u64>() as u64).min(u64::from(MAX_PDF_OBJECTS));
let next_capacity = if next_count > self.offsets.capacity() {
let doubled = if self.offsets.capacity() == 0 {
4
} else {
self.offsets.capacity().saturating_mul(2)
};
doubled.min(max_slots as usize).max(next_count)
} else {
self.offsets.capacity()
};
let bytes = next_capacity
.checked_mul(size_of::<u64>())
.ok_or(Error::invalid(
"PDF object index size overflows address space",
))?;
let bytes = len_u64(bytes);
limits.check_allocation(bytes)?;
if next_count > self.offsets.capacity() {
let refused = limits.allocation_refused("PDF object index allocation", bytes);
let additional = next_capacity - self.offsets.len();
reserve_exact(&mut self.offsets, additional, refused)?;
}
Ok(())
}
pub fn begin_object(&mut self, id: ObjectId) -> Result<()> {
self.ensure_idle()?;
let index = self.unwritten_index(id)?;
let offset = self.out.position;
let header = format!("{} 0 obj\n", id.number());
self.out.write(header.as_bytes())?;
self.offsets[index] = offset;
self.state = State::Object;
Ok(())
}
pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> {
self.out.ensure_healthy()?;
if self.state != State::Object {
return Err(Error::invalid("PDF bytes require an open ordinary object"));
}
self.out.write(bytes)
}
pub fn end_object(&mut self) -> Result<()> {
self.out.ensure_healthy()?;
if self.state != State::Object {
return Err(Error::invalid("no ordinary PDF object is open"));
}
self.out.write(b"\nendobj\n")?;
self.state = State::Idle;
Ok(())
}
pub fn write_object(&mut self, id: ObjectId, body: &[u8]) -> Result<()> {
self.begin_object(id)?;
self.write_bytes(body)?;
self.end_object()
}
pub fn begin_stream(
&mut self,
stream_id: ObjectId,
length_id: ObjectId,
dictionary_entries: &[u8],
) -> Result<()> {
self.ensure_idle()?;
if stream_id == length_id {
return Err(Error::invalid(
"PDF stream and length objects must be distinct",
));
}
self.unwritten_index(length_id)?;
self.begin_object(stream_id)?;
self.write_bytes(b"<<\n/Length ")?;
self.write_bytes(length_id.number().to_string().as_bytes())?;
self.write_bytes(b" 0 R\n")?;
self.write_bytes(dictionary_entries)?;
if !dictionary_entries.is_empty() && !dictionary_entries.ends_with(b"\n") {
self.write_bytes(b"\n")?;
}
self.write_bytes(b">>\nstream\n")?;
self.state = State::Stream {
length_id,
data_start: self.out.position,
};
Ok(())
}
pub fn write_stream_bytes(&mut self, bytes: &[u8]) -> Result<()> {
self.out.ensure_healthy()?;
let State::Stream { data_start, .. } = self.state else {
return Err(Error::invalid("no PDF stream is open"));
};
let current_length = self
.out
.position
.checked_sub(data_start)
.ok_or(Error::invalid("PDF stream position moved backwards"))?;
let chunk_length = len_u64(bytes.len());
let attempted = current_length
.checked_add(chunk_length)
.ok_or(Error::invalid("PDF stream length overflows 64 bits"))?;
if attempted > MAX_PDF_INTEGER {
return Err(Error::limit(
"PDF stream length",
MAX_PDF_INTEGER,
attempted,
));
}
self.out.write(bytes)
}
pub fn end_stream(&mut self) -> Result<()> {
self.out.ensure_healthy()?;
let State::Stream {
length_id,
data_start,
} = self.state
else {
return Err(Error::invalid("no PDF stream is open"));
};
let length = self
.out
.position
.checked_sub(data_start)
.ok_or(Error::invalid("PDF stream position moved backwards"))?;
self.out.write(b"\nendstream\nendobj\n")?;
self.state = State::Idle;
self.write_object(length_id, length.to_string().as_bytes())
}
pub fn finish(self, root_id: ObjectId) -> Result<u64> {
self.finish_with_info(root_id, None)
}
pub fn finish_with_info(mut self, root_id: ObjectId, info_id: Option<ObjectId>) -> Result<u64> {
self.ensure_idle()?;
let root_index = self.index(root_id)?;
if let Some(id) = info_id {
self.index(id)?;
}
if self.offsets[root_index] == 0 {
return Err(Error::invalid("PDF catalog object has not been written"));
}
if self.offsets.contains(&0) {
return Err(Error::invalid("a reserved PDF object has not been written"));
}
let entries = self
.offsets
.iter()
.enumerate()
.map(|(index, &offset)| (PdfRef::from(ObjectId(index as u32 + 1)), offset));
let trailer = Trailer {
size: len_u64(self.offsets.len()) + 1,
root: root_id.into(),
prev: None,
info: info_id.map(PdfRef::from),
id: None,
};
let start = self.out.position;
let final_size = dense_xref_len(&trailer, start)
.and_then(|tail| start.checked_add(tail))
.ok_or(Error::invalid("PDF output byte count overflows 64 bits"))?;
check_classic_pdf_bytes(final_size)?;
let limits = self.out.limits;
if final_size > limits.max_output_bytes {
return Err(Error::limit(
"output bytes",
limits.max_output_bytes,
final_size,
));
}
write_xref(&mut self.out, entries, true, &trailer)?;
self.out.flush()?;
Ok(self.out.position)
}
pub(crate) fn ensure_idle(&self) -> Result<()> {
self.out.ensure_healthy()?;
if self.state != State::Idle {
return Err(Error::invalid("another PDF object is already open"));
}
Ok(())
}
fn index(&self, id: ObjectId) -> Result<usize> {
let Some(index) = id.number().checked_sub(1) else {
return Err(Error::invalid("PDF object number zero is reserved"));
};
let index = index as usize;
if index >= self.offsets.len() {
return Err(Error::invalid("PDF object number was not reserved"));
}
Ok(index)
}
fn unwritten_index(&self, id: ObjectId) -> Result<usize> {
let index = self.index(id)?;
if self.offsets[index] != 0 {
return Err(Error::invalid("PDF object has already been written"));
}
Ok(index)
}
}
impl<W: Write, C: Cancellation> ObjectSink for PdfWriter<'_, W, C> {
type Ref = ObjectId;
fn begin_object(&mut self, reference: ObjectId) -> Result<()> {
PdfWriter::begin_object(self, reference)
}
fn write(&mut self, bytes: &[u8]) -> Result<()> {
self.write_bytes(bytes)
}
fn end_object(&mut self) -> Result<()> {
PdfWriter::end_object(self)
}
}
impl<W: Write, C: Cancellation> ObjectAllocator for PdfWriter<'_, W, C> {
fn reserve(&mut self) -> Result<ObjectId> {
self.reserve_object()
}
}
pub(crate) fn check_classic_pdf_bytes(attempted: u64) -> Result<()> {
if attempted > MAX_CLASSIC_PDF_BYTES {
return Err(Error::limit(
"classic PDF file bytes",
MAX_CLASSIC_PDF_BYTES,
attempted,
));
}
Ok(())
}
pub(crate) fn checked_object_number(count: usize) -> Result<u32> {
let attempted = len_u64(count);
if attempted > u64::from(MAX_PDF_OBJECTS) {
return Err(Error::limit(
"PDF object count",
u64::from(MAX_PDF_OBJECTS),
attempted,
));
}
Ok(count as u32)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ErrorKind;
use crate::NeverCancel;
fn vec_sink() -> Vec<u8> {
Vec::new()
}
#[test]
fn preflighted_objects_do_not_mint_ids_and_overflow_is_recoverable() {
let mut sink = vec_sink();
let limits = Limits::default();
let mut writer = PdfWriter::new(&mut sink, &limits, &NeverCancel).unwrap();
let before = writer.position();
writer.prepare_objects(5).unwrap();
assert!(writer.offsets.is_empty());
assert_eq!(writer.position(), before);
assert_eq!(writer.reserve_object().unwrap().number(), 1);
assert!(matches!(
writer.prepare_objects(usize::MAX),
Err(Error {
kind: ErrorKind::Malformed,
..
})
));
assert_eq!(writer.offsets.len(), 1);
assert_eq!(writer.position(), before);
assert_eq!(writer.reserve_object().unwrap().number(), 2);
}
#[test]
fn classic_output_ends_within_ten_digit_offsets() {
assert!(check_classic_pdf_bytes(MAX_CLASSIC_PDF_BYTES).is_ok());
assert!(matches!(
check_classic_pdf_bytes(MAX_CLASSIC_PDF_BYTES + 1),
Err(Error { kind: ErrorKind::LimitExceeded { resource: "classic PDF file bytes", limit: MAX_CLASSIC_PDF_BYTES, attempted }, .. }) if attempted == MAX_CLASSIC_PDF_BYTES + 1
));
}
#[test]
fn object_count_stays_within_annex_c_reader_profile() {
assert_eq!(
checked_object_number(MAX_PDF_OBJECTS as usize).unwrap(),
MAX_PDF_OBJECTS
);
assert!(matches!(
checked_object_number(MAX_PDF_OBJECTS as usize + 1),
Err(Error {
kind: ErrorKind::LimitExceeded {
resource: "PDF object count",
limit: 8_388_607,
attempted: 8_388_608
},
..
})
));
}
#[test]
fn output_ceiling_is_checked_before_writing() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let object = pdf.reserve_object()?;
pdf.begin_object(object)?;
pdf.set_position_for_test(MAX_CLASSIC_PDF_BYTES);
assert!(matches!(
pdf.write_bytes(b"x"),
Err(Error {
kind: ErrorKind::LimitExceeded {
resource: "classic PDF file bytes",
..
},
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn stream_integer_limit_is_checked_before_writing() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let stream = pdf.reserve_object()?;
let length = pdf.reserve_object()?;
pdf.begin_stream(stream, length, b"")?;
assert!(
matches!(pdf.state, State::Stream { .. }),
"stream should be open"
);
if let State::Stream { data_start, .. } = pdf.state {
pdf.set_position_for_test(data_start + MAX_PDF_INTEGER);
}
assert!(matches!(
pdf.write_stream_bytes(b"x"),
Err(Error {
kind: ErrorKind::LimitExceeded {
resource: "PDF stream length",
..
},
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn stream_bytes_need_an_open_stream_and_are_written_verbatim() {
let mut sink = vec_sink();
(|| {
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
assert!(matches!(
pdf.write_stream_bytes(b"x"),
Err(Error {
kind: ErrorKind::Malformed,
reason: "no PDF stream is open",
..
})
));
let stream = pdf.reserve_object()?;
let length = pdf.reserve_object()?;
pdf.begin_stream(stream, length, b"")?;
let start = pdf.position();
pdf.write_stream_bytes(b"endstream\0")?;
assert_eq!(pdf.position(), start + 10);
Ok::<(), Error>(())
})()
.unwrap();
assert!(sink.ends_with(b"stream\nendstream\0"));
}
#[test]
fn minimal_pdf_byte_count_includes_xref_and_trailer() {
let mut sink = vec_sink();
let written = (|| {
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
pdf.write_object(catalog, b"<< >>")?;
assert_eq!(pdf.position(), 15 + 21);
pdf.finish(catalog)
})()
.unwrap();
let output = sink;
assert_eq!(written, output.len() as u64);
let xref = output
.windows(b"\nxref\n".len())
.position(|window| window == b"\nxref\n")
.unwrap()
+ 1;
assert!(output.ends_with(
format!(
"\nxref\n0 2\n0000000000 65535 f \n0000000015 00000 n \ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n"
)
.as_bytes()
));
}
#[test]
fn finish_requires_a_written_catalog() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
assert!(matches!(
pdf.finish(catalog),
Err(Error {
kind: ErrorKind::Malformed,
reason: "PDF catalog object has not been written",
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn finish_requires_every_reserved_object_and_fits_the_output_limit() {
let mut sink = vec_sink();
let limits = Limits::default();
(|| {
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
pdf.reserve_object()?;
pdf.write_object(catalog, b"<< >>")?;
assert!(matches!(
pdf.finish(catalog),
Err(Error {
kind: ErrorKind::Malformed,
reason: "a reserved PDF object has not been written",
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
let mut sink = vec_sink();
let limits = Limits {
max_output_bytes: 100,
..Limits::default()
};
let written = (|| {
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
pdf.write_object(catalog, b"<< >>")?;
let written = pdf.position();
assert!(matches!(
pdf.finish(catalog),
Err(Error { kind: ErrorKind::LimitExceeded { resource: "output bytes", limit: 100, attempted }, .. }) if attempted > 100
));
Ok::<u64, Error>(written)
})()
.unwrap();
assert_eq!(sink.len() as u64, written);
}
#[test]
fn the_xref_preflight_length_matches_the_written_table() {
let mut sink = vec_sink();
let limits = Limits::default();
let (start, written) = (|| {
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
let info = pdf.reserve_object()?;
pdf.write_object(catalog, b"<< >>")?;
pdf.write_object(info, b"<< >>")?;
let start = pdf.position();
Ok::<_, Error>((start, pdf.finish_with_info(catalog, Some(info))?))
})()
.unwrap();
let trailer = Trailer {
size: 3,
root: ObjectId(1).into(),
prev: None,
info: Some(ObjectId(2).into()),
id: None,
};
assert_eq!(dense_xref_len(&trailer, start), Some(written - start));
}
#[test]
fn info_reference_must_be_reserved() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
pdf.write_object(catalog, b"<< /Type /Catalog >>")?;
assert!(matches!(
pdf.finish_with_info(catalog, Some(ObjectId(catalog.number() + 1))),
Err(Error {
kind: ErrorKind::Malformed,
reason: "PDF object number was not reserved",
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn unreserved_object_numbers_are_rejected() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let reserved = pdf.reserve_object()?;
let before = pdf.position();
assert!(matches!(
pdf.begin_object(ObjectId(reserved.number() + 1)),
Err(Error {
kind: ErrorKind::Malformed,
reason: "PDF object number was not reserved",
..
})
));
assert_eq!(pdf.position(), before);
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn xref_and_trailer_are_checked_against_the_classic_ceiling() {
(|| {
let mut sink = vec_sink();
let limits = Limits {
max_output_bytes: u64::MAX,
..Limits::default()
};
let mut pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
let catalog = pdf.reserve_object()?;
pdf.write_object(catalog, b"<< >>")?;
pdf.set_position_for_test(MAX_CLASSIC_PDF_BYTES - 20);
assert!(matches!(
pdf.finish(catalog),
Err(Error { kind: ErrorKind::LimitExceeded { resource: "classic PDF file bytes", limit: MAX_CLASSIC_PDF_BYTES, attempted }, .. }) if attempted > MAX_CLASSIC_PDF_BYTES
));
Ok::<(), Error>(())
})()
.unwrap();
}
#[test]
fn object_zero_cannot_be_referenced() {
(|| {
let mut sink = vec_sink();
let limits = Limits::default();
let pdf = PdfWriter::new(&mut sink, &limits, &NeverCancel)?;
assert!(matches!(
pdf.index(ObjectId(0)),
Err(Error {
kind: ErrorKind::Malformed,
..
})
));
Ok::<(), Error>(())
})()
.unwrap();
}
}