use super::inflate::{ExactInflate, InflateFault, InflateFaultKind};
use super::{Header, Location, PageRecord, Span, Variant};
use crate::fallible::{len_u64, reserve_exact, usize_from_u32};
use crate::{Cancellation, Error, ErrorKind, Limits, RangedSource, Result, read_exact_at};
mod raw;
mod records;
use records::{ImageRegions, Records};
const HEADER_BYTES: usize = 24;
const CHUNK_BYTES: usize = 64 * 1024;
pub const TEXT_DECODER_RESERVATION_BYTES: u64 = 128 * 1024;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RawTextCoordinate {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
#[derive(Debug, Eq, PartialEq)]
pub struct TextCoordinates {
pub text: Span,
pub page_size: Option<[u16; 2]>,
pub zlib_frame: Option<Span>,
pub decoded_length: u32,
pub record_count: u32,
pub coordinates: Vec<RawTextCoordinate>,
pub max_source_request_bytes: usize,
pub max_decoder_output_chunk_bytes: usize,
pub owned_buffer_bytes: u64,
}
pub(super) enum PageText {
Framed(TextCoordinates),
Unframed(Error),
}
pub(super) fn read_coordinates<S: RangedSource, C: Cancellation>(
source: &mut S,
header: Header,
page: PageRecord,
limits: &Limits,
cancellation: &C,
) -> Result<TextCoordinates> {
match read_page_text(source, header, page, limits, cancellation)? {
PageText::Framed(text) => Ok(text),
PageText::Unframed(error) => Err(error),
}
}
pub(super) fn read_page_text<S: RangedSource, C: Cancellation>(
source: &mut S,
header: Header,
page: PageRecord,
limits: &Limits,
cancellation: &C,
) -> Result<PageText> {
if header.variant == Variant::HnB {
return Err(location(header, page).unsupported("text framing variant"));
}
let loc = location(header, page);
validate_metadata(header, page, source.size(), limits, loc)?;
if page.text.length < HEADER_BYTES as u64 {
return Ok(PageText::Unframed(loc.truncated(
"page text header",
HEADER_BYTES as u64,
page.text.length,
)));
}
let mut prefix = [0; 4];
read_chunks(
source,
page.text.offset,
&mut prefix,
limits,
cancellation,
loc,
&mut 0,
)?;
let tag = u16::from_le_bytes([prefix[0], prefix[1]]);
let mut prefixed_raw = false;
let mut page_size = None;
if header.variant == Variant::HnA && tag == 0x8003 {
let mut following = [0; 16];
read_chunks(
source,
page.text.offset + 4,
&mut following,
limits,
cancellation,
loc,
&mut 0,
)?;
if following[..2] == [0x03, 0x80] {
page_size = Some([
u16::from_le_bytes([prefix[2], prefix[3]]),
u16::from_le_bytes([following[2], following[3]]),
]);
}
prefixed_raw = following[..2] == [0x03, 0x80]
&& matches!(
u16::from_le_bytes([following[4], following[5]]),
0x800a | 0x801c
);
}
let raw_c8_image = header.variant == Variant::C8
&& page.image_count == 1
&& tag == 0x800a
&& prefix[2..4] == [0, 0];
let mut text = if raw_c8_image
|| (header.variant == Variant::HnA
&& (prefixed_raw || matches!(tag, 0x8001 | 0x800a | 0x8004)))
{
let records = if prefixed_raw || tag == 0x800a {
Records::tagged(prefixed_raw).decode_markers(header.variant == Variant::HnA)
} else {
Records::ordered()
};
raw::read(source, page, limits, cancellation, loc, records)?
} else {
match read_compressed_text(source, header, page, limits, cancellation)? {
PageText::Framed(text) => text,
unframed => return Ok(unframed),
}
};
text.page_size = page_size;
Ok(PageText::Framed(text))
}
fn location(header: Header, page: PageRecord) -> Location {
Location {
variant: Some(header.variant),
offset: page.text.offset,
page: Some(page.page_number),
image: None,
}
}
fn checked_end(
span: Span,
size: u64,
loc: Location,
field: &'static str,
overflow: &'static str,
) -> Result<u64> {
let end = span
.checked_end()
.ok_or_else(|| loc.at(span.offset).malformed(overflow))?;
if end > size {
return Err(loc.at(span.offset).truncated(
field,
span.length,
size.saturating_sub(span.offset),
));
}
Ok(end)
}
fn validate_metadata(
header: Header,
page: PageRecord,
size: u64,
limits: &Limits,
loc: Location,
) -> Result<()> {
if size > limits.max_input_bytes {
return Err(loc.limit("source bytes", limits.max_input_bytes, size));
}
if header.page_count == 0 || header.page_count > i32::MAX as u32 {
return Err(loc.malformed("page count: outside positive signed 32-bit range"));
}
if header.page_count > limits.max_pages {
return Err(loc.limit(
"pages",
u64::from(limits.max_pages),
u64::from(header.page_count),
));
}
if page.page_number == 0 || page.page_number > header.page_count {
return Err(loc.malformed("page number: outside declared page index"));
}
if header.page_index.length != u64::from(header.page_count) * super::PAGE_ROW_BYTES {
return Err(loc.malformed("page index: length differs from declared row count"));
}
let index_end = checked_end(
header.page_index,
size,
loc,
"page index",
"page index: end overflows u64",
)?;
let index_start_valid = if header.variant == Variant::C8 {
header.page_index.offset == 0x50
} else {
header.page_index.offset >= 0x15c
&& (header.page_index.offset - 0x15c).is_multiple_of(super::OUTLINE_RECORD_BYTES)
&& (header.page_index.offset - 0x15c) / super::OUTLINE_RECORD_BYTES <= i32::MAX as u64
};
if !index_start_valid {
return Err(loc.malformed("page index: start differs from observed variant layout"));
}
let row = header.page_index.offset + u64::from(page.page_number - 1) * super::PAGE_ROW_BYTES;
if page.row_offset != row {
return Err(loc
.at(page.row_offset)
.malformed("page row: offset differs from declared index"));
}
checked_end(
page.text,
size,
loc,
"page text span",
"page text span: end overflows u64",
)?;
if page.text.offset < index_end {
return Err(loc.malformed("page text span: overlaps protected container index"));
}
if page.text.offset > i32::MAX as u64 || page.text.length > i32::MAX as u64 {
return Err(loc.malformed("page text span: outside nonnegative signed 32-bit range"));
}
Ok(())
}
fn read_chunks<S: RangedSource, C: Cancellation>(
source: &mut S,
offset: u64,
bytes: &mut [u8],
limits: &Limits,
cancellation: &C,
loc: Location,
max_request: &mut usize,
) -> Result<()> {
let mut current = offset;
for chunk in bytes.chunks_mut(limits.io_chunk_bytes.min(CHUNK_BYTES)) {
*max_request = (*max_request).max(chunk.len());
read_exact_at(source, current, chunk, limits, cancellation).map_err(|error| match error
.kind
{
ErrorKind::Truncated { available, .. } => loc
.at(current.saturating_add(available))
.truncated("page text read", len_u64(chunk.len()), available),
_ => loc.at(current).locate(error),
})?;
current += len_u64(chunk.len());
}
Ok(())
}
fn allocate<T: Clone>(count: usize, value: T, limits: &Limits, loc: Location) -> Result<Vec<T>> {
let bytes = len_u64(count).saturating_mul(len_u64(size_of::<T>()));
if bytes > limits.max_allocation_bytes {
return Err(loc.limit("text allocation bytes", limits.max_allocation_bytes, bytes));
}
let mut result = Vec::new();
let failure = loc.limit("text allocation bytes", limits.max_allocation_bytes, bytes);
reserve_exact(&mut result, count, failure)?;
result.resize(count, value);
Ok(result)
}
fn read_compressed_text<S: RangedSource, C: Cancellation>(
source: &mut S,
header: Header,
page: PageRecord,
limits: &Limits,
cancellation: &C,
) -> Result<PageText> {
let loc = location(header, page);
let mut max_source_request_bytes = 0;
let mut fixed = [0; HEADER_BYTES];
read_chunks(
source,
page.text.offset,
&mut fixed,
limits,
cancellation,
loc,
&mut max_source_request_bytes,
)?;
let header_bytes = if &fixed[..12] == b"COMPRESSTEXT" {
16
} else if fixed[..2] == [0x03, 0x80]
&& fixed[4..6] == [0x03, 0x80]
&& &fixed[8..20] == b"COMPRESSTEXT"
{
HEADER_BYTES
} else {
return Ok(PageText::Unframed(loc.malformed(
"page text prefix: unsupported compressed text header",
)));
};
let decoded_length = u32::from_le_bytes(
fixed[header_bytes - 4..header_bytes]
.try_into()
.expect("fixed field width"),
);
let decoded_bytes = u64::from(decoded_length);
if decoded_bytes > limits.max_output_bytes {
return Err(loc.limit(
"text decoded output bytes",
limits.max_output_bytes,
decoded_bytes,
));
}
let mut tail_bytes = u64::from(page.image_count) * 28;
let mut image_regions = None;
let (glyph_bytes, record_count) = if header_bytes == HEADER_BYTES {
let mut glyph_bytes = decoded_bytes.checked_sub(12 + tail_bytes).ok_or_else(|| {
loc.malformed("decoded text layout: too short for declared image records")
})?;
let regions = match (header.variant, page.image_count, glyph_bytes % 16) {
(Variant::HnA, 1, 12) => 1,
(Variant::HnA, 1, 8) => 2,
_ => 0,
};
if regions != 0 {
let region_bytes = u64::from(regions) * 28;
glyph_bytes = glyph_bytes
.checked_sub(region_bytes)
.ok_or_else(|| loc.malformed("decoded text layout: too short for image regions"))?;
let mut kind = [0; 4];
read_chunks(
source,
page.text.offset + page.text.length,
&mut kind,
limits,
cancellation,
loc,
&mut max_source_request_bytes,
)?;
if u32::from_le_bytes(kind) != 2 {
return Err(loc.malformed("decoded image regions require one JPEG descriptor"));
}
let ordinal = u16::try_from(page.page_number - 1)
.map_err(|_| loc.unsupported("image region page ordinal"))?;
image_regions = Some(ImageRegions {
count: regions,
page_size: [
u16::from_le_bytes([fixed[2], fixed[3]]),
u16::from_le_bytes([fixed[6], fixed[7]]),
],
ordinal,
});
tail_bytes += region_bytes;
}
if glyph_bytes % 16 != 0 {
return Err(loc.malformed("decoded text layout: record area is not a multiple of 16"));
}
let record_count = (glyph_bytes / 16) as u32;
(glyph_bytes, record_count)
} else {
(0, 0)
};
let zlib_frame = Span {
offset: page.text.offset + header_bytes as u64,
length: page.text.length - header_bytes as u64,
};
if zlib_frame.length < 6 {
return Err(loc
.at(zlib_frame.offset)
.truncated("text zlib frame", 6, zlib_frame.length));
}
let chunk = limits.io_chunk_bytes.min(CHUNK_BYTES);
let input_count = zlib_frame.length.min(len_u64(chunk)) as usize;
let output_count = (decoded_bytes + 1).min(len_u64(chunk)) as usize;
if TEXT_DECODER_RESERVATION_BYTES > limits.max_allocation_bytes {
return Err(loc.limit(
"text decoder allocation reservation",
limits.max_allocation_bytes,
TEXT_DECODER_RESERVATION_BYTES,
));
}
let mut input = allocate(input_count, 0_u8, limits, loc)?;
let mut output = allocate(output_count, 0_u8, limits, loc)?;
let images = usize_from_u32(page.image_count);
let mut coordinates = allocate(images, RawTextCoordinate::default(), limits, loc)?;
let mut records = if header_bytes == 16 {
Records::tagged(false)
} else {
Records::fixed(
8 + glyph_bytes,
decoded_bytes.saturating_sub(tail_bytes),
record_count,
)
.decode_markers(header.variant == Variant::HnA)
.image_regions(image_regions)
};
let frame_loc = loc.at(zlib_frame.offset);
let owned_buffer_bytes = len_u64(input.capacity())
.saturating_add(len_u64(output.capacity()))
.saturating_add(
len_u64(coordinates.capacity()).saturating_mul(size_of::<RawTextCoordinate>() as u64),
);
let mut inflate = ExactInflate::new(zlib_frame.offset, zlib_frame.length, decoded_bytes);
let fault = |fault: InflateFault| {
let reason = match fault.kind {
InflateFaultKind::Invalid => "text zlib frame: invalid stream, dictionary or checksum",
InflateFaultKind::Excess => "decoded text length: output exceeds declared length",
InflateFaultKind::EndMismatch => {
"text zlib frame: end differs from declared compressed/decoded span"
}
InflateFaultKind::Stalled => {
"text zlib frame: truncated stream or decoder made no progress"
}
};
loc.at(fault.offset).malformed(reason)
};
let mut max_decoder_output_chunk_bytes = 0;
loop {
if cancellation.is_cancelled() {
return Err(loc.at(inflate.position()).cancelled());
}
if let Some((at, length)) = inflate.next_read(input.len()) {
read_chunks(
source,
at,
&mut input[..length],
limits,
cancellation,
loc,
&mut max_source_request_bytes,
)?;
}
let writable = inflate.writable(output.len());
let step = inflate
.step(&input, &mut output[..writable])
.map_err(fault)?;
if cancellation.is_cancelled() {
return Err(loc.at(inflate.position()).cancelled());
}
inflate.check_length(&step).map_err(fault)?;
max_decoder_output_chunk_bytes = max_decoder_output_chunk_bytes.max(step.produced);
records.consume(
step.before_out,
&output[..step.produced],
&mut coordinates,
frame_loc,
)?;
if inflate.finished(&step).map_err(fault)? {
break;
}
}
let record_count = records.finish(&mut coordinates, frame_loc)?;
Ok(PageText::Framed(TextCoordinates {
text: page.text,
page_size: None,
zlib_frame: Some(zlib_frame),
decoded_length,
record_count,
coordinates,
max_source_request_bytes,
max_decoder_output_chunk_bytes,
owned_buffer_bytes,
}))
}
#[cfg(test)]
mod tests;