use pdfrum_cmap::{CharCode, CidSet, Words};
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity, hex_digit};
use smallvec::SmallVec;
use std::collections::{BTreeMap, HashMap};
const CID_LIMIT: u32 = 0xffff;
const OUT_OF_SPEC_BF_LIMIT: i64 = 160_000;
const MAX_RUN_SPAN: u32 = 256;
#[derive(Debug, Clone, Default)]
pub struct ToUnicode {
singles: BTreeMap<u32, u32>,
reverse_singles: BTreeMap<u32, u32>,
runs: Vec<Run>,
runs_by_start: Vec<Run>,
multi_char: Vec<Vec<u32>>,
base_set: CidSet,
}
#[derive(Debug, Clone, Copy)]
struct Run {
low: u32,
high: u32,
start: u32,
}
impl Run {
const fn value_at(self, code: u32) -> Option<u32> {
if code < self.low || code > self.high {
return None;
}
Some(self.start.wrapping_add(code - self.low))
}
const fn code_at(self, value: u32) -> Option<u32> {
let offset = value.wrapping_sub(self.start);
if offset > self.high - self.low {
return None;
}
Some(self.low + offset)
}
fn pairs(self) -> impl Iterator<Item = (u32, u32)> {
(self.low..=self.high).map(move |code| (code, self.start.wrapping_add(code - self.low)))
}
}
impl ToUnicode {
fn forward(&self, code: u32) -> Option<u32> {
let from_runs =
Self::window(&self.runs, |run| run.low, code).filter_map(|run| run.value_at(code));
self.singles
.get(&code)
.copied()
.into_iter()
.chain(from_runs)
.min()
}
fn window<K: Fn(&Run) -> u32>(runs: &[Run], key: K, target: u32) -> impl Iterator<Item = &Run> {
let first = target.saturating_sub(MAX_RUN_SPAN - 1);
let start = runs.partition_point(|run| key(run) < first);
runs.get(start..)
.unwrap_or_default()
.iter()
.take_while(move |run| key(run) <= target)
}
fn reverse_code(&self, value: u32) -> Option<u32> {
let from_runs = Self::window(&self.runs_by_start, |run| run.start, value)
.filter_map(|run| run.code_at(value));
self.reverse_singles
.get(&value)
.copied()
.into_iter()
.chain(from_runs)
.min()
}
fn reverse_entries(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
let mut merged: BTreeMap<u32, u32> = self.reverse_singles.clone();
for run in &self.runs {
for (code, value) in run.pairs() {
merged
.entry(value)
.and_modify(|c| *c = (*c).min(code))
.or_insert(code);
}
}
merged.into_iter()
}
#[must_use]
pub fn lookup(&self, code: CharCode) -> SmallVec<[char; 2]> {
let Some(value) = self.forward(code.0) else {
if self.base_set == CidSet::Unknown {
return SmallVec::new();
}
let cid = pdfrum_cmap::Cid(u16::try_from(code.0 & 0xffff).unwrap_or(0));
let ch = pdfrum_cmap::unicode_from_cid(self.base_set, cid).unwrap_or('\0');
return SmallVec::from_slice(&[ch]);
};
let unit = value & 0xffff;
if unit != 0xffff {
return units_to_chars(&[unit]);
}
let index = (value >> 16) as usize;
self.multi_char
.get(index)
.map_or_else(SmallVec::new, |units| units_to_chars(units))
}
#[must_use]
pub fn reverse(&self, unicode: char) -> CharCode {
CharCode(self.reverse_code(unicode as u32).unwrap_or(0))
}
pub fn reverse_pairs(&self) -> impl Iterator<Item = (char, u32)> + '_ {
self.reverse_entries()
.filter_map(|(unicode, code)| Some((char::from_u32(unicode)?, code)))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.singles.is_empty() && self.runs.is_empty() && self.base_set == CidSet::Unknown
}
#[cfg(test)]
#[must_use]
pub fn len(&self) -> usize {
let mut codes: std::collections::BTreeSet<u32> = self.singles.keys().copied().collect();
for run in &self.runs {
codes.extend(run.low..=run.high);
}
codes.len()
}
#[cfg(test)]
#[must_use]
pub fn base_set(&self) -> CidSet {
self.base_set
}
#[cfg(test)]
fn unicode_count(&self, charcode: u32) -> usize {
self.reverse_entries()
.filter(|&(_, c)| c == charcode)
.count()
}
fn insert(&mut self, code: u32, destcode: u32) {
self.singles
.entry(code)
.and_modify(|v| *v = (*v).min(destcode))
.or_insert(destcode);
self.reverse_singles
.entry(destcode)
.and_modify(|c| *c = (*c).min(code))
.or_insert(code);
}
fn insert_run(&mut self, run: Run) {
self.runs.push(run);
}
fn seal(&mut self) {
self.runs.sort_unstable_by_key(|run| run.low);
self.runs_by_start.clone_from(&self.runs);
self.runs_by_start.sort_unstable_by_key(|run| run.start);
}
fn multi_char_indicator(&self) -> u32 {
u32::try_from(self.multi_char.len())
.ok()
.and_then(|n| n.checked_mul(0x10000))
.and_then(|n| n.checked_add(0xffff))
.unwrap_or(0)
}
fn set_code(&mut self, srccode: u32, dest: &[u32]) {
match dest {
[] => {}
[single] => self.insert(srccode, *single),
multi => {
self.insert(srccode, self.multi_char_indicator());
self.multi_char.push(multi.to_vec());
}
}
}
}
fn units_to_chars(units: &[u32]) -> SmallVec<[char; 2]> {
let mut out = SmallVec::new();
let mut i = 0;
while let Some(&unit) = units.get(i) {
i += 1;
if unit > 0xffff {
out.push(char::from_u32(unit).unwrap_or(char::REPLACEMENT_CHARACTER));
continue;
}
if (0xd800..0xdc00).contains(&unit)
&& let Some(&low @ 0xdc00..=0xdfff) = units.get(i)
{
i += 1;
let scalar = 0x1_0000 + ((unit - 0xd800) << 10) + (low - 0xdc00);
out.push(char::from_u32(scalar).unwrap_or(char::REPLACEMENT_CHARACTER));
continue;
}
out.push(char::from_u32(unit).unwrap_or(char::REPLACEMENT_CHARACTER));
}
out
}
#[must_use]
pub fn parse(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> ToUnicode {
let mut map = ToUnicode::default();
let mut words = Words::new(bytes);
let mut previous: Vec<u8> = Vec::new();
let mut pending = words.next().map(<[u8]>::to_vec);
while let Some(word) = pending.take() {
if word.is_empty() {
break;
}
let next = match word.as_slice() {
b"beginbfchar" => Some(handle_bfchar(
&mut words, &previous, limits, &mut map, diags,
)),
b"beginbfrange" => Some(handle_bfrange(
&mut words, &previous, limits, &mut map, diags,
)),
b"/Adobe-Korea1-UCS2" => {
map.base_set = CidSet::Korea1;
None
}
b"/Adobe-Japan1-UCS2" => {
map.base_set = CidSet::Japan1;
None
}
b"/Adobe-CNS1-UCS2" => {
map.base_set = CidSet::Cns1;
None
}
b"/Adobe-GB1-UCS2" => {
map.base_set = CidSet::Gb1;
None
}
_ => None,
};
previous = next.unwrap_or(word);
pending = words.next().map(<[u8]>::to_vec);
}
map.seal();
map
}
fn string_to_code(word: &[u8]) -> Option<u32> {
if word.len() <= 2 || word.first() != Some(&b'<') || word.last() != Some(&b'>') {
return None;
}
let mut code: u32 = 0;
for &c in word.get(1..word.len() - 1)? {
if is_pdf_whitespace(c) {
continue;
}
let digit = hex_digit(c)?;
code = code.checked_mul(16)?.checked_add(u32::from(digit))?;
}
Some(code)
}
fn string_to_units(word: &[u8]) -> Vec<u32> {
if word.len() <= 2 || word.first() != Some(&b'<') || word.last() != Some(&b'>') {
return Vec::new();
}
let Some(body) = word.get(1..word.len() - 1) else {
return Vec::new();
};
let mut result = Vec::new();
let mut byte_pos = 0u8;
let mut ch: u32 = 0;
for &c in body {
if is_pdf_whitespace(c) {
continue;
}
let Some(digit) = hex_digit(c) else {
break;
};
ch = ch * 16 + u32::from(digit);
byte_pos += 1;
if byte_pos == 4 {
result.push(ch);
byte_pos = 0;
ch = 0;
}
}
result
}
fn is_pdf_whitespace(c: u8) -> bool {
matches!(c, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20)
}
fn declared_count(previous: &[u8]) -> (bool, usize) {
let raw = parse_int(previous);
let valid = (0..=OUT_OF_SPEC_BF_LIMIT).contains(&raw);
(valid, if valid { raw as usize } else { 0 })
}
fn parse_int(word: &[u8]) -> i64 {
let (negative, digits) = match word.first() {
Some(b'-') => (true, word.get(1..).unwrap_or_default()),
Some(b'+') => (false, word.get(1..).unwrap_or_default()),
_ => (false, word),
};
let mut value: i64 = 0;
for &c in digits {
let Some(d) = c.checked_sub(b'0').filter(|d| *d <= 9) else {
break;
};
let Some(next) = value
.checked_mul(10)
.and_then(|v| v.checked_add(i64::from(d)))
else {
return 0;
};
value = next;
}
if negative { -value } else { value }
}
fn handle_bfchar(
words: &mut Words<'_>,
previous: &[u8],
limits: &Limits,
map: &mut ToUnicode,
diags: &mut Diagnostics,
) -> Vec<u8> {
let (mut is_valid, expected) = declared_count(previous);
let mut collected: Vec<(u32, Vec<u32>)> = Vec::new();
let mut last = Vec::new();
while let Some(word) = words.next() {
if word.is_empty() || word == b"endbfchar" {
last = word.to_vec();
break;
}
if !is_valid {
continue;
}
match string_to_code(word) {
Some(code) if code <= CID_LIMIT => {
let Some(dest) = words.next() else { break };
collected.push((code, string_to_units(dest)));
if collected.len() > expected || collected.len() > limits.max_array_len {
is_valid = false;
}
}
_ => is_valid = false,
}
}
if is_valid && collected.len() == expected {
for (code, dest) in &collected {
map.set_code(*code, dest);
}
} else if !collected.is_empty() || expected != 0 {
diags.record(Severity::Suspicious, DiagKind::ToUnicodeBlockRejected, None);
}
last
}
enum Range {
Array { low: u32, dests: Vec<Vec<u32>> },
Consecutive(Run),
Incremented { low: u32, dests: Vec<Vec<u32>> },
}
fn handle_bfrange(
words: &mut Words<'_>,
previous: &[u8],
limits: &Limits,
map: &mut ToUnicode,
diags: &mut Diagnostics,
) -> Vec<u8> {
let (mut is_valid, expected) = declared_count(previous);
let mut ranges: Vec<Range> = Vec::new();
let mut last = Vec::new();
while let Some(word) = words.next() {
if word.is_empty() || word == b"endbfrange" {
last = word.to_vec();
break;
}
if !is_valid {
continue;
}
let Some(lowcode) = string_to_code(word) else {
is_valid = false;
continue;
};
let Some(high_word) = words.next() else { break };
let Some(highraw) = string_to_code(high_word) else {
is_valid = false;
continue;
};
let highcode = (lowcode & 0xffff_ff00) | (highraw & 0xff);
if lowcode > CID_LIMIT || highcode > CID_LIMIT || lowcode > highcode {
is_valid = false;
continue;
}
let span = (highcode - lowcode) as usize + 1;
let Some(third) = words.next() else { break };
if third == b"[" {
let mut dests = Vec::with_capacity(span.min(256));
for _ in 0..span {
let Some(w) = words.next() else { break };
dests.push(string_to_units(w));
}
ranges.push(Range::Array {
low: lowcode,
dests,
});
if ranges.len() > expected {
is_valid = false;
continue;
}
match words.next() {
Some(b"]") => {}
_ => is_valid = false,
}
continue;
}
let dest = string_to_units(third);
if let [single] = dest.as_slice() {
ranges.push(Range::Consecutive(Run {
low: lowcode,
high: highcode,
start: *single,
}));
} else {
let mut dests = Vec::with_capacity(span.min(256));
dests.push(dest);
for _ in lowcode + 1..=highcode {
let next = dests.last().map_or_else(Vec::new, |d| string_data_add(d));
dests.push(next);
}
ranges.push(Range::Incremented {
low: lowcode,
dests,
});
}
if ranges.len() > expected || ranges.len() > limits.max_array_len {
is_valid = false;
}
}
if is_valid && ranges.len() == expected {
for range in &ranges {
commit_range(range, map);
}
} else if !ranges.is_empty() || expected != 0 {
diags.record(Severity::Suspicious, DiagKind::ToUnicodeBlockRejected, None);
}
last
}
fn commit_range(range: &Range, map: &mut ToUnicode) {
match range {
Range::Array { low, dests } => {
for (i, dest) in dests.iter().enumerate() {
let Some(code) = u32::try_from(i).ok().and_then(|i| low.checked_add(i)) else {
break;
};
map.set_code(code, dest);
}
}
Range::Consecutive(run) => {
map.insert_run(*run);
}
Range::Incremented { low, dests } => {
for (i, dest) in dests.iter().enumerate() {
let Some(code) = u32::try_from(i).ok().and_then(|i| low.checked_add(i)) else {
break;
};
map.set_code(code, dest);
}
}
}
}
fn string_data_add(units: &[u32]) -> Vec<u32> {
let mut out: Vec<u32> = Vec::with_capacity(units.len() + 1);
let mut value: u32 = 1;
for &unit in units.iter().rev() {
let ch = unit.wrapping_add(value);
if ch < unit {
out.push(0);
} else {
out.push(ch);
value = 0;
}
}
if value != 0 {
out.push(value);
}
out.reverse();
out
}
#[cfg(test)]
#[path = "tounicode_tests.rs"]
mod tests;
#[must_use]
pub fn invert_to_unicode(
bytes: &[u8],
limits: &Limits,
diags: &mut Diagnostics,
) -> HashMap<char, u32> {
parse(bytes, limits, diags)
.reverse_pairs()
.filter(|(_, code)| *code != 0)
.collect()
}