use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use crate::decode::{CodeRange, Decoder};
use crate::ids::{CidSet, CodingScheme};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CidRange {
pub(crate) start_code: u32,
pub(crate) end_code: u32,
pub(crate) start_cid: u16,
}
pub(crate) const DIRECT_TABLE_SIZE: usize = 0x1_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Status {
Start,
CidChar,
CidRange,
Registry,
Ordering,
Supplement,
WMode,
CodeSpaceRange,
}
pub(crate) struct Parsed {
pub(crate) decoder: Decoder,
pub(crate) direct: DirectTable,
pub(crate) additional: Vec<CidRange>,
pub(crate) charset: CidSet,
pub(crate) vertical: bool,
pub(crate) use_cmap: Option<Vec<u8>>,
pub(crate) declared_codespace: bool,
}
struct Builder<'a> {
status: Status,
code_seq: u32,
code_points: [u32; 4],
ranges: Vec<CodeRange>,
pending_ranges: Vec<CodeRange>,
additional: Vec<CidRange>,
direct: DirectTable,
last_word: &'a [u8],
scheme: CodingScheme,
charset: CidSet,
vertical: bool,
ranges_capped: bool,
use_cmap: Option<Vec<u8>>,
declared_codespace: bool,
}
fn zeroed_table() -> DirectTable {
DirectTable(vec![0u16; DIRECT_TABLE_SIZE])
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DirectTable(Vec<u16>);
impl DirectTable {
pub(crate) fn get(&self, code: u32) -> Option<u16> {
usize::try_from(code)
.ok()
.and_then(|i| self.0.get(i))
.copied()
}
fn set(&mut self, code: u32, cid: u16) -> bool {
let Ok(i) = usize::try_from(code) else {
return false;
};
match self.0.get_mut(i) {
Some(slot) => {
*slot = cid;
true
}
None => false,
}
}
}
pub(crate) fn parse(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> Parsed {
let mut b = Builder {
status: Status::Start,
code_seq: 0,
code_points: [0; 4],
ranges: Vec::new(),
pending_ranges: Vec::new(),
additional: Vec::new(),
direct: zeroed_table(),
last_word: &[],
scheme: CodingScheme::default(),
charset: CidSet::Unknown,
vertical: false,
ranges_capped: false,
use_cmap: None,
declared_codespace: false,
};
for word in crate::lexer::Words::new(bytes) {
b.feed(word, limits, diags);
}
b.finish(diags)
}
impl<'a> Builder<'a> {
fn feed(&mut self, word: &'a [u8], limits: &Limits, diags: &mut Diagnostics) {
match word {
b"begincidchar" => {
self.status = Status::CidChar;
self.code_seq = 0;
}
b"begincidrange" => {
self.status = Status::CidRange;
self.code_seq = 0;
}
b"endcidrange" | b"endcidchar" => self.status = Status::Start,
b"/WMode" => self.status = Status::WMode,
b"/Registry" => self.status = Status::Registry,
b"/Ordering" => self.status = Status::Ordering,
b"/Supplement" => self.status = Status::Supplement,
b"begincodespacerange" => {
self.status = Status::CodeSpaceRange;
self.code_seq = 0;
}
b"usecmap" => {
if self.use_cmap.is_none() {
self.use_cmap = Some(self.last_word.to_vec());
}
}
_ => match self.status {
Status::CidChar | Status::CidRange => self.handle_cid(word, limits, diags),
Status::Registry | Status::Supplement => self.status = Status::Start,
Status::Ordering => {
self.charset = crate::charset_from_ordering(operand_value(word));
self.status = Status::Start;
}
Status::WMode => {
self.vertical = get_code(word) != 0;
self.status = Status::Start;
}
Status::CodeSpaceRange => self.handle_codespace(word, limits, diags),
Status::Start => {}
},
}
self.last_word = word;
}
fn handle_cid(&mut self, word: &'a [u8], limits: &Limits, diags: &mut Diagnostics) {
let is_char = self.status == Status::CidChar;
let seq = self.code_seq as usize;
let Some(slot) = self.code_points.get_mut(seq) else {
self.code_seq = 0;
diags.record(Severity::Suspicious, DiagKind::CMapOperandOverflow, None);
return;
};
*slot = get_code(word);
self.code_seq += 1;
let required = if is_char { 2 } else { 3 };
if self.code_seq < required {
return;
}
let cp = self.code_points;
let (start, end, cid) = if is_char {
(cp[0], cp[0], cp[1] as u16)
} else {
(cp[0], cp[1], cp[2] as u16)
};
self.code_seq = 0;
if usize::try_from(end).is_ok_and(|e| e < DIRECT_TABLE_SIZE) {
if start > end {
diags.record(Severity::Suspicious, DiagKind::CMapReversedRange, None);
return;
}
for code in start..=end {
let mapped = (u32::from(cid).wrapping_add(code).wrapping_sub(start)) as u16;
if !self.direct.set(code, mapped) {
break;
}
}
} else if self.additional.len() >= limits.max_cmap_ranges {
if !self.ranges_capped {
self.ranges_capped = true;
diags.record(Severity::Suspicious, DiagKind::CMapRangeLimit, None);
}
} else {
self.additional.push(CidRange {
start_code: start,
end_code: end,
start_cid: cid,
});
}
}
fn handle_codespace(&mut self, word: &'a [u8], limits: &Limits, diags: &mut Diagnostics) {
if word != b"endcodespacerange" {
if word.first() != Some(&b'<') {
return;
}
if self.code_seq % 2 == 1
&& let Some(range) = get_code_range(self.last_word, word, diags)
{
if self.pending_ranges.len() >= limits.max_cmap_ranges {
if !self.ranges_capped {
self.ranges_capped = true;
diags.record(Severity::Suspicious, DiagKind::CMapRangeLimit, None);
}
} else {
self.pending_ranges.push(range);
}
}
self.code_seq += 1;
return;
}
self.end_codespace(diags);
}
fn end_codespace(&mut self, diags: &mut Diagnostics) {
let segs = self.ranges.len() + self.pending_ranges.len();
self.declared_codespace |= segs > 0;
if segs == 1 {
let width = self
.ranges
.first()
.or(self.pending_ranges.first())
.map_or(0, |r| r.char_size);
self.scheme = if width == 2 {
CodingScheme::TwoBytes
} else {
CodingScheme::OneByte
};
diags.record(Severity::Recovered, DiagKind::CMapCodespaceDropped, None);
} else if segs > 1 {
self.scheme = CodingScheme::MixedFourBytes;
self.ranges.append(&mut self.pending_ranges);
}
self.status = Status::Start;
}
fn finish(mut self, diags: &mut Diagnostics) -> Parsed {
let additional = if self.scheme == CodingScheme::MixedFourBytes {
self.additional.sort_by_key(|r| r.end_code);
self.additional
} else {
if !self.additional.is_empty() {
diags.record(
Severity::Suspicious,
DiagKind::CMapWideMappingsDropped,
None,
);
}
Vec::new()
};
let decoder = match self.scheme {
CodingScheme::OneByte => Decoder::OneByte,
CodingScheme::MixedFourBytes => Decoder::MixedFourBytes {
ranges: self.ranges,
},
CodingScheme::TwoBytes | CodingScheme::MixedTwoBytes => Decoder::TwoBytes,
};
Parsed {
decoder,
direct: self.direct,
additional,
charset: self.charset,
vertical: self.vertical,
use_cmap: self.use_cmap,
declared_codespace: self.declared_codespace,
}
}
}
fn operand_value(word: &[u8]) -> &[u8] {
word.get(2..).unwrap_or_default()
}
pub(crate) fn get_code(word: &[u8]) -> u32 {
let (digits, radix) = match word.first() {
None => return 0,
Some(&b'<') => (word.get(1..).unwrap_or_default(), 16u32),
Some(_) => (word, 10),
};
let mut num = 0u32;
for &b in digits {
let Some(d) = char::from(b).to_digit(radix) else {
break;
};
let Some(next) = num.checked_mul(radix).and_then(|n| n.checked_add(d)) else {
return 0;
};
num = next;
}
num
}
fn hex_digit(b: u8) -> u8 {
char::from(b).to_digit(16).unwrap_or(0) as u8
}
pub(crate) fn get_code_range(
first: &[u8],
second: &[u8],
diags: &mut Diagnostics,
) -> Option<CodeRange> {
if first.first() != Some(&b'<') {
return None;
}
let close = first
.iter()
.position(|&b| b == b'>')
.filter(|&i| i >= 1)
.unwrap_or(first.len());
if close == first.len() {
diags.record(Severity::Suspicious, DiagKind::CMapTruncatedCodespace, None);
}
let char_size = (close.saturating_sub(1)) / 2;
if char_size > 4 {
return None;
}
let mut range = CodeRange {
char_size: u8::try_from(char_size).unwrap_or(4),
lower: [0; 4],
upper: [0; 4],
};
for i in 0..char_size {
let hi = first.get(i * 2 + 1).copied().map_or(0, hex_digit);
let lo = first.get(i * 2 + 2).copied().map_or(0, hex_digit);
if let Some(slot) = range.lower.get_mut(i) {
*slot = hi * 16 + lo;
}
let hi = second.get(i * 2 + 1).copied().map_or(0, hex_digit);
let lo = second.get(i * 2 + 2).copied().map_or(0, hex_digit);
if let Some(slot) = range.upper.get_mut(i) {
*slot = hi * 16 + lo;
}
}
Some(range)
}
#[cfg(test)]
mod tests {
use super::{get_code, get_code_range, operand_value};
use pdfrum_common::Diagnostics;
#[test]
fn get_code_matches_the_pinned_assertions() {
for (input, want) in [
(&b""[..], 0u32),
(b"<", 0),
(b"<c2", 194),
(b"<A2", 162),
(b"<Af2", 2802),
(b"<A2z", 162),
(b"12", 12),
(b"12d", 12),
(b"128", 128),
(b"<FFFFFFFF", 4_294_967_295),
(b"<100000000", 0),
] {
assert_eq!(get_code(input), want, "GetCode({input:?})");
}
}
#[test]
fn get_code_stops_at_the_closing_bracket() {
assert_eq!(get_code(b"<A2>"), 162);
assert_eq!(get_code(b"<20>"), 0x20);
assert_eq!(get_code(b"<>"), 0);
assert_eq!(get_code(b"1234567890123"), 0);
assert_eq!(get_code(b"abc"), 0);
}
#[test]
fn get_code_range_matches_the_pinned_assertions() {
let mut d = Diagnostics::default();
assert!(get_code_range(b"", b"", &mut d).is_none());
assert!(get_code_range(b"A", b"", &mut d).is_none());
assert!(get_code_range(b"<aaaaaaaaaa>", b"", &mut d).is_none());
let r = get_code_range(b"<12345678>", b"<87654321>", &mut d).unwrap();
assert_eq!(r.char_size, 4);
assert_eq!(r.lower, [18, 52, 86, 120]);
assert_eq!(r.upper, [135, 101, 67, 33]);
let r = get_code_range(b"<a1>", b"<F3>", &mut d).unwrap();
assert_eq!(r.char_size, 1);
assert_eq!(r.lower[0], 161);
assert_eq!(r.upper[0], 243);
let r = get_code_range(b"<a1>", b"", &mut d).unwrap();
assert_eq!(r.char_size, 1);
assert_eq!(r.lower[0], 161);
assert_eq!(r.upper[0], 0);
}
#[test]
fn unterminated_bounds_still_produce_a_range() {
let mut d = Diagnostics::default();
let r = get_code_range(b"<a1", b"<f3>", &mut d).unwrap();
assert_eq!(r.char_size, 1);
assert_eq!(r.lower[0], 0xa1);
assert_eq!(r.upper[0], 0xf3);
assert!(d.contains(&pdfrum_common::DiagKind::CMapTruncatedCodespace));
}
#[test]
fn non_hex_digits_read_as_zero() {
let mut d = Diagnostics::default();
let r = get_code_range(b"<zz>", b"<zz>", &mut d).unwrap();
assert_eq!(r.char_size, 1);
assert_eq!(r.lower[0], 0);
assert_eq!(r.upper[0], 0);
}
#[test]
fn width_zero_ranges_are_representable() {
let mut d = Diagnostics::default();
let r = get_code_range(b"<>", b"<>", &mut d).unwrap();
assert_eq!(r.char_size, 0);
}
#[test]
fn operand_value_drops_two_bytes() {
assert_eq!(operand_value(b"(Japan1)"), b"apan1)");
assert_eq!(operand_value(b"ab"), b"");
assert_eq!(operand_value(b""), b"");
assert_eq!(operand_value(b"abJapan1"), b"Japan1");
}
}