mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
fix(render): vendor hayro-postscript so a run of << in a CMap can't overflow the stack (#4363)
hayro-cmap reads CMap streams with hayro-postscript's token scanner, whose object::read skipped each `<<` and `>>` by calling itself again. A CMap holding a long run of `<` or `>` used one stack frame per pair, overflowed the stack and aborted the process. Vendor hayro-postscript 0.1.0 with read() looping over the delimiters instead (from storytold/craft-libs#263), patched in beside hayro-cmap. Four object::tests cover long `<<` and `>>` runs and the skipped delimiters, plain and nested.
This commit is contained in:
+90
@@ -0,0 +1,90 @@
|
||||
// Keep in sync with `hayro-syntax/src/filter/ascii_85.rs`.
|
||||
|
||||
use crate::reader::{Reader, is_whitespace};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1];
|
||||
|
||||
let mut reader = Reader::new(data);
|
||||
|
||||
let mut read_byte = || -> Option<u8> {
|
||||
loop {
|
||||
let b = reader.read_byte()?;
|
||||
|
||||
// White space characters should be ignored.
|
||||
if !is_whitespace(b) {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let flush_group = |group: &mut Vec<u8>, decoded: &mut Vec<u8>| -> Option<()> {
|
||||
let (digits, output_len): ([u32; 5], usize) = match group.len() {
|
||||
0 => return Some(()),
|
||||
1 => return None, // A single character is not valid.
|
||||
2 => (
|
||||
[group[0], group[1], b'u', b'u', b'u'].map(|b| (b - b'!') as u32),
|
||||
1,
|
||||
),
|
||||
3 => (
|
||||
[group[0], group[1], group[2], b'u', b'u'].map(|b| (b - b'!') as u32),
|
||||
2,
|
||||
),
|
||||
4 => (
|
||||
[group[0], group[1], group[2], group[3], b'u'].map(|b| (b - b'!') as u32),
|
||||
3,
|
||||
),
|
||||
5 => (
|
||||
[group[0], group[1], group[2], group[3], group[4]].map(|b| (b - b'!') as u32),
|
||||
4,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let value = digits[0]
|
||||
.checked_mul(POW_85[0])?
|
||||
.checked_add(digits[1].checked_mul(POW_85[1])?)?
|
||||
.checked_add(digits[2].checked_mul(POW_85[2])?)?
|
||||
.checked_add(digits[3].checked_mul(POW_85[3])?)?
|
||||
.checked_add(digits[4])?;
|
||||
|
||||
decoded.extend_from_slice(&value.to_be_bytes()[..output_len]);
|
||||
group.clear();
|
||||
Some(())
|
||||
};
|
||||
|
||||
out.reserve(data.len() * 4 / 5);
|
||||
let mut group = Vec::with_capacity(5);
|
||||
|
||||
loop {
|
||||
let Some(b) = read_byte() else {
|
||||
// Be lenient and accept what we have (see PDFBOX-5910).
|
||||
flush_group(&mut group, out)?;
|
||||
|
||||
return Some(());
|
||||
};
|
||||
|
||||
match b {
|
||||
b'!'..=b'u' => {
|
||||
group.push(b);
|
||||
|
||||
if group.len() == 5 {
|
||||
flush_group(&mut group, out)?;
|
||||
}
|
||||
}
|
||||
b'z' => {
|
||||
flush_group(&mut group, out)?;
|
||||
out.extend_from_slice(&[0, 0, 0, 0]);
|
||||
}
|
||||
b'~' => {
|
||||
// Technically requires a '>', but there is a PDF where it isn't
|
||||
// appended and decodes fine in other viewers.
|
||||
flush_group(&mut group, out)?;
|
||||
|
||||
return Some(());
|
||||
}
|
||||
_ => return None, // Invalid character.
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Keep in sync with `hayro-syntax/src/filter/ascii_hex.rs`.
|
||||
|
||||
use crate::reader::is_whitespace;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
let has_whitespace = data.iter().any(|&b| is_whitespace(b));
|
||||
|
||||
out.reserve(data.len().div_ceil(2));
|
||||
|
||||
if !has_whitespace {
|
||||
// Fast path, don't need to worry about white spaces.
|
||||
let mut i = 0;
|
||||
while i + 1 < data.len() {
|
||||
out.push(decode_hex_digit(data[i])? << 4 | decode_hex_digit(data[i + 1])?);
|
||||
i += 2;
|
||||
}
|
||||
if i < data.len() {
|
||||
out.push(decode_hex_digit(data[i])? << 4);
|
||||
}
|
||||
} else {
|
||||
// Slow path, need to strip white spaces.
|
||||
let mut iter = data.iter().copied();
|
||||
let mut read_byte = || -> Option<u8> {
|
||||
loop {
|
||||
let b = iter.next()?;
|
||||
if !is_whitespace(b) {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match (read_byte(), read_byte()) {
|
||||
(Some(hi), Some(lo)) => {
|
||||
out.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?);
|
||||
}
|
||||
(Some(hi), None) => {
|
||||
out.push(decode_hex_digit(hi)? << 4);
|
||||
|
||||
break;
|
||||
}
|
||||
(None, _) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn decode_hex_digit(c: u8) -> Option<u8> {
|
||||
match c {
|
||||
b'0'..=b'9' => Some(c - b'0'),
|
||||
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Keep in sync with `hayro-syntax/src/object/string.rs` (`read_literal`).
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::reader::Reader;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
let mut r = Reader::new(data);
|
||||
|
||||
while let Some(byte) = r.read_byte() {
|
||||
match byte {
|
||||
b'\\' => {
|
||||
let next = r.read_byte()?;
|
||||
|
||||
if is_octal_digit(next) {
|
||||
let second = r.read_byte();
|
||||
let third = r.read_byte();
|
||||
|
||||
let bytes = match (second, third) {
|
||||
(Some(n1), Some(n2)) => match (is_octal_digit(n1), is_octal_digit(n2)) {
|
||||
(true, true) => [next, n1, n2],
|
||||
(true, _) => {
|
||||
r.jump(r.offset() - 1);
|
||||
[b'0', next, n1]
|
||||
}
|
||||
_ => {
|
||||
r.jump(r.offset() - 2);
|
||||
[b'0', b'0', next]
|
||||
}
|
||||
},
|
||||
(Some(n1), None) => {
|
||||
if is_octal_digit(n1) {
|
||||
[b'0', next, n1]
|
||||
} else {
|
||||
r.jump(r.offset() - 1);
|
||||
[b'0', b'0', next]
|
||||
}
|
||||
}
|
||||
_ => [b'0', b'0', next],
|
||||
};
|
||||
|
||||
let str = core::str::from_utf8(&bytes).unwrap();
|
||||
|
||||
if let Ok(num) = u8::from_str_radix(str, 8) {
|
||||
out.push(num);
|
||||
}
|
||||
} else {
|
||||
match next {
|
||||
b'n' => out.push(0xA),
|
||||
b'r' => out.push(0xD),
|
||||
b't' => out.push(0x9),
|
||||
b'b' => out.push(0x8),
|
||||
b'f' => out.push(0xC),
|
||||
b'(' => out.push(b'('),
|
||||
b')' => out.push(b')'),
|
||||
b'\\' => out.push(b'\\'),
|
||||
b'\n' | b'\r' => {
|
||||
// "If the \ is followed immediately by a newline
|
||||
// (CR, LF, or CR-LF pair), the scanner ignores
|
||||
// both the initial \ and the newline; this breaks
|
||||
// a string into multiple lines without including
|
||||
// the newline character as part of the string."
|
||||
r.skip_eol();
|
||||
}
|
||||
_ => out.push(next),
|
||||
}
|
||||
}
|
||||
}
|
||||
b'(' | b')' => out.push(byte),
|
||||
// "But if a newline appears without a preceding \, the result is
|
||||
// equivalent to \n."
|
||||
b'\n' | b'\r' => {
|
||||
out.push(b'\n');
|
||||
r.skip_eol();
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn is_octal_digit(byte: u8) -> bool {
|
||||
matches!(byte, b'0'..=b'7')
|
||||
}
|
||||
Reference in New Issue
Block a user