use std::collections::BTreeSet;
type Codespace = (usize, u32, u32);
type Entry = (usize, u32, u32, u32);
pub(crate) type Code = (usize, u32);
#[derive(Debug, Clone, Default)]
pub(crate) struct CodeCidMap {
codespace: Vec<Codespace>,
ranges: Vec<Entry>,
chars: Vec<Entry>,
notdefs: Vec<Entry>,
base: Option<Box<CodeCidMap>>,
}
const MAX_CODE_BYTES: usize = 4;
fn hex_value(token: &str) -> Option<(usize, u32)> {
let body = token.strip_prefix('<')?.strip_suffix('>')?;
if body.is_empty() || body.len() > MAX_CODE_BYTES * 2 || !body.len().is_multiple_of(2) {
return None;
}
Some((body.len() / 2, u32::from_str_radix(body, 16).ok()?))
}
impl CodeCidMap {
pub(crate) fn parse(
data: &[u8],
resolve: &dyn Fn(&str) -> Option<Vec<u8>>,
) -> Option<CodeCidMap> {
Self::parse_at_depth(data, resolve, 0)
}
fn parse_at_depth(
data: &[u8],
resolve: &dyn Fn(&str) -> Option<Vec<u8>>,
depth: usize,
) -> Option<CodeCidMap> {
if depth > 4 {
return None;
}
let text = String::from_utf8_lossy(data);
let mut map = CodeCidMap::default();
let tokens: Vec<&str> = text
.lines()
.map(|line| line.split('%').next().unwrap_or(""))
.flat_map(str::split_whitespace)
.collect();
for (i, token) in tokens.iter().enumerate() {
if *token == "usecmap" {
let named = tokens.get(i.checked_sub(1)?)?.strip_prefix('/')?;
if map.base.is_some() {
return None;
}
let base = CodeCidMap::parse_at_depth(&resolve(named)?, resolve, depth + 1)?;
map.codespace.extend(base.codespace.iter().copied());
map.base = Some(Box::new(base));
}
}
let mut cursor = 0usize;
while cursor < tokens.len() {
let opener = tokens[cursor];
cursor += 1;
let (closer, arity) = match opener {
"begincodespacerange" => ("endcodespacerange", 2),
"begincidrange" => ("endcidrange", 3),
"beginnotdefrange" => ("endnotdefrange", 3),
"begincidchar" => ("endcidchar", 2),
"beginnotdefchar" => ("endnotdefchar", 2),
"beginbfchar" | "beginbfrange" => return None,
_ => continue,
};
let end = tokens[cursor..]
.iter()
.position(|t| *t == closer)
.map(|at| cursor + at)?;
let body = &tokens[cursor..end];
cursor = end + 1;
if !body.len().is_multiple_of(arity) {
return None;
}
for entry in body.chunks(arity) {
match opener {
"begincodespacerange" => {
let (bytes, low) = hex_value(entry[0])?;
let (high_bytes, high) = hex_value(entry[1])?;
if bytes != high_bytes || low > high {
return None;
}
map.codespace.push((bytes, low, high));
}
"begincidrange" | "beginnotdefrange" => {
let (bytes, low) = hex_value(entry[0])?;
let (high_bytes, high) = hex_value(entry[1])?;
if bytes != high_bytes {
return None;
}
let cid: u32 = entry[2].parse().ok()?;
if cid > u32::from(u16::MAX) {
return None;
}
if low <= high {
let into = if opener == "begincidrange" {
&mut map.ranges
} else {
&mut map.notdefs
};
into.push((bytes, low, high, cid));
}
}
other => {
let (bytes, code) = hex_value(entry[0])?;
let cid: u32 = entry[1].parse().ok()?;
if cid > u32::from(u16::MAX) {
return None;
}
if other == "beginnotdefchar" {
map.notdefs.push((bytes, code, code, cid));
} else {
map.chars.push((bytes, code, code, cid));
}
}
}
}
}
(!map.codespace.is_empty()).then_some(map)
}
fn code_length(&self, bytes: &[u8]) -> Option<usize> {
for len in 1..=MAX_CODE_BYTES.min(bytes.len()) {
let value = bytes[..len]
.iter()
.fold(0u32, |acc, byte| (acc << 8) | u32::from(*byte));
if self
.codespace
.iter()
.any(|(width, low, high)| *width == len && (*low..=*high).contains(&value))
{
return Some(len);
}
}
None
}
pub(crate) fn cids_of(&self, codes: &BTreeSet<Code>, out: &mut BTreeSet<u32>) {
out.extend(codes.iter().map(|code| self.cid(*code)));
}
pub(crate) fn produces(&self, cid: u32) -> bool {
let hit = |table: &[Entry], run: bool| {
table.iter().any(|(_, low, high, first)| {
let last = if run {
first.saturating_add(high - low)
} else {
*first
};
(*first..=last).contains(&cid)
})
};
cid == 0
|| hit(&self.ranges, true)
|| hit(&self.chars, false)
|| hit(&self.notdefs, false)
|| self.base.as_ref().is_some_and(|base| base.produces(cid))
}
pub(crate) fn produced_cids(&self, out: &mut BTreeSet<u32>) {
let mut spans = Vec::new();
self.cid_spans(&mut spans);
spans.sort_unstable();
let mut open: Option<(u32, u32)> = None;
for (low, high) in spans {
match open {
Some((_, last)) if low <= last.saturating_add(1) => {
open = open.map(|(first, last)| (first, last.max(high)));
}
Some((first, last)) => {
out.extend(first..=last);
open = Some((low, high));
}
None => open = Some((low, high)),
}
}
if let Some((first, last)) = open {
out.extend(first..=last);
}
}
fn cid_spans(&self, out: &mut Vec<(u32, u32)>) {
const LAST_CID: u32 = u16::MAX as u32;
out.push((0, 0));
for (_, low, high, first) in &self.ranges {
if *first > LAST_CID {
continue;
}
out.push((*first, first.saturating_add(high - low).min(LAST_CID)));
}
for table in [&self.chars, &self.notdefs] {
out.extend(
table
.iter()
.map(|(_, _, _, cid)| *cid)
.filter(|cid| *cid <= LAST_CID)
.map(|cid| (cid, cid)),
);
}
if let Some(base) = &self.base {
base.cid_spans(out);
}
}
fn lookup(&self, code: Code) -> Option<u32> {
let (bytes, value) = code;
let hit = |table: &[Entry], run: bool| {
table
.iter()
.rev()
.find(|(width, low, high, _)| *width == bytes && (*low..=*high).contains(&value))
.map(|(_, low, _, cid)| if run { cid + (value - low) } else { *cid })
};
hit(&self.ranges, true)
.or_else(|| hit(&self.chars, false))
.or_else(|| hit(&self.notdefs, false))
.or_else(|| self.base.as_ref()?.lookup(code))
}
fn cid(&self, code: Code) -> u32 {
self.lookup(code).unwrap_or(0)
}
pub(crate) fn codes(&self, bytes: &[u8], out: &mut BTreeSet<Code>) -> Option<()> {
let mut at = 0;
while at < bytes.len() {
let len = self.code_length(&bytes[at..])?;
out.insert((
len,
bytes[at..at + len]
.iter()
.fold(0u32, |acc, byte| (acc << 8) | u32::from(*byte)),
));
at += len;
}
Some(())
}
#[cfg(test)]
pub(crate) fn decode(&self, bytes: &[u8], out: &mut BTreeSet<u32>) -> Option<()> {
let mut codes = BTreeSet::new();
self.codes(bytes, &mut codes)?;
self.cids_of(&codes, out);
Some(())
}
pub(crate) fn codes_or_superset(&self, bytes: &[u8], out: &mut BTreeSet<Code>) {
if self.codes(bytes, out).is_some() {
return;
}
let widths: BTreeSet<usize> = self.codespace.iter().map(|(width, _, _)| *width).collect();
for at in 0..bytes.len() {
for width in &widths {
let Some(slice) = bytes.get(at..at + width) else {
continue;
};
out.insert((
*width,
slice
.iter()
.fold(0u32, |acc, byte| (acc << 8) | u32::from(*byte)),
));
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
const BUNDLED: &[(&str, &[u8])] = &[
(
"GB-EUC-H",
include_bytes!("../resources/cmap/GB-EUC-H").as_slice(),
),
(
"GBK-EUC-H",
include_bytes!("../resources/cid-tables/GBK-EUC-H"),
),
(
"GBKp-EUC-H",
include_bytes!("../resources/cid-tables/GBKp-EUC-H"),
),
(
"90ms-RKSJ-H",
include_bytes!("../resources/cid-tables/90ms-RKSJ-H"),
),
(
"UniGB-UTF16-H",
include_bytes!("../resources/cid-tables/UniGB-UTF16-H"),
),
(
"UniJIS-UTF16-H",
include_bytes!("../resources/cid-tables/UniJIS-UTF16-H"),
),
];
#[cfg(target_arch = "wasm32")]
const BUNDLED: &[(&str, &[u8])] = &[];
fn bundled(name: &str) -> Option<Vec<u8>> {
BUNDLED
.iter()
.find(|(known, _)| *known == name)
.map(|(_, data)| data.to_vec())
}
pub(crate) fn predefined(name: &str) -> Option<CodeCidMap> {
CodeCidMap::parse(&bundled(name)?, &bundled)
}
#[cfg(test)]
mod tests {
use super::*;
const MIXED: &str = "\
/CIDInit /ProcSet findresource begin
2 begincodespacerange
<00> <80>
<8140> <FEFE>
endcodespacerange
1 beginnotdefrange
<00> <1f> 1
endnotdefrange
2 begincidrange
<20> <7e> 814
<8140> <8140> 7000
endcidrange
1 begincidchar
<8141> 7777
endcidchar
endcmap
";
fn decode(map: &CodeCidMap, bytes: &[u8]) -> Option<Vec<u32>> {
let mut out = BTreeSet::new();
map.decode(bytes, &mut out)?;
Some(out.into_iter().collect())
}
#[test]
fn a_one_byte_code_is_not_half_of_a_two_byte_one() {
let map = CodeCidMap::parse(MIXED.as_bytes(), &|_| None).expect("parse");
assert_eq!(decode(&map, &[0x41]), Some(vec![814 + 0x41 - 0x20]));
assert_eq!(decode(&map, &[0x81, 0x40]), Some(vec![7000]));
assert_eq!(decode(&map, &[0x81, 0x41]), Some(vec![7777]));
assert_eq!(
decode(&map, &[0x41, 0x81, 0x40]),
Some(vec![814 + 0x41 - 0x20, 7000])
);
}
#[test]
fn a_child_cmap_overrides_the_base_across_mapping_kinds() {
let base = "1 begincodespacerange\n<00> <ff>\nendcodespacerange\n\
1 begincidrange\n<41> <45> 100\nendcidrange\n";
let child = "/Base usecmap\n1 begincodespacerange\n<00> <ff>\nendcodespacerange\n\
1 begincidchar\n<42> 900\nendcidchar\n";
let map = CodeCidMap::parse(child.as_bytes(), &|name| {
(name == "Base").then(|| base.as_bytes().to_vec())
})
.expect("parse with base");
assert_eq!(decode(&map, &[0x42]), Some(vec![900]), "the child wins");
assert_eq!(
decode(&map, &[0x41]),
Some(vec![100]),
"the base still answers the rest"
);
assert_eq!(
decode(&map, &[0x43]),
Some(vec![102]),
"and a run keeps running"
);
}
#[test]
fn a_notdef_range_never_masks_a_real_mapping() {
let text = "\
1 begincodespacerange
<00> <ff>
endcodespacerange
1 begincidchar
<41> 900
endcidchar
1 beginnotdefrange
<00> <ff> 1
endnotdefrange
";
let map = CodeCidMap::parse(text.as_bytes(), &|_| None).expect("parse");
assert_eq!(decode(&map, &[0x41]), Some(vec![900]), "the cidchar wins");
assert_eq!(
decode(&map, &[0x42]),
Some(vec![1]),
"and .notdef covers the rest"
);
}
#[test]
fn a_cmap_that_maps_with_bfchar_is_refused_rather_than_half_read() {
for section in [
"1 beginbfchar\n<41> <0050>\nendbfchar\n",
"1 beginbfrange\n<41> <45> <0050>\nendbfrange\n",
] {
let text = format!(
"1 begincodespacerange\n<00> <ff>\nendcodespacerange\n\
1 begincidrange\n<20> <2f> 100\nendcidrange\n{section}"
);
assert!(
CodeCidMap::parse(text.as_bytes(), &|_| None).is_none(),
"a table that maps codes this file cannot read is not a table"
);
}
}
#[test]
fn a_one_byte_code_and_a_two_byte_code_of_the_same_value_are_not_one_code() {
let text = "\
2 begincodespacerange
<81> <81>
<0080> <00ff>
endcodespacerange
2 begincidrange
<80> <80> 5
<0080> <0080> 900
endcidrange
1 begincidchar
<81> 7
endcidchar
";
let map = CodeCidMap::parse(text.as_bytes(), &|_| None).expect("parse");
assert_eq!(decode(&map, &[0x00, 0x80]), Some(vec![900]));
assert_eq!(decode(&map, &[0x81]), Some(vec![7]));
let mut cids = BTreeSet::new();
map.cids_of(&BTreeSet::from([(1usize, 0x80u32)]), &mut cids);
assert_eq!(cids.into_iter().collect::<Vec<_>>(), vec![5]);
}
#[test]
fn a_cid_outside_the_cid_range_is_not_a_table() {
for section in [
"1 begincidchar\n<41> 2147483648\nendcidchar\n",
"1 begincidrange\n<41> <45> 70000\nendcidrange\n",
] {
let text = format!("1 begincodespacerange\n<00> <ff>\nendcodespacerange\n{section}");
assert!(CodeCidMap::parse(text.as_bytes(), &|_| None).is_none());
}
}
#[test]
fn a_notdef_range_is_one_cid_and_not_a_run() {
let map = CodeCidMap::parse(MIXED.as_bytes(), &|_| None).expect("parse");
assert_eq!(decode(&map, &[0x00, 0x1f]), Some(vec![1]));
}
#[test]
fn a_byte_outside_the_codespace_answers_nothing() {
let map = CodeCidMap::parse(MIXED.as_bytes(), &|_| None).expect("parse");
assert_eq!(decode(&map, &[0x81]), None);
assert_eq!(decode(&map, &[0x81, 0x00]), None);
}
#[test]
fn a_usecmap_that_cannot_be_resolved_is_not_half_a_table() {
let text = "/Base usecmap\n1 begincodespacerange\n<00> <ff>\nendcodespacerange\n";
assert!(CodeCidMap::parse(text.as_bytes(), &|_| None).is_none());
let base = "1 begincodespacerange\n<00> <ff>\nendcodespacerange\n\
1 begincidrange\n<00> <ff> 5\nendcidrange\n";
let map = CodeCidMap::parse(text.as_bytes(), &|name| {
(name == "Base").then(|| base.as_bytes().to_vec())
})
.expect("parse with base");
assert_eq!(decode(&map, &[0x02]), Some(vec![7]));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn the_bundled_tables_answer_the_codes_their_documents_draw() {
let gbkp = predefined("GBKp-EUC-H").expect("GBKp-EUC-H is bundled");
let mut cids = BTreeSet::new();
gbkp.decode(
&[
0xCE, 0xD2, 0xC3, 0xC7, 0xB6, 0xBC, 0xCA, 0xC7, 0xBA, 0xDA, 0xCC, 0xE5, 0xD7, 0xD6,
],
&mut cids,
)
.expect("every byte is in the codespace");
assert_eq!(
cids.into_iter().collect::<Vec<_>>(),
vec![1531, 1937, 2764, 3422, 3640, 3809, 4659]
);
for name in [
"GB-EUC-H",
"GBK-EUC-H",
"90ms-RKSJ-H",
"UniGB-UTF16-H",
"UniJIS-UTF16-H",
] {
assert!(predefined(name).is_some(), "{name} did not parse");
}
assert!(predefined("Identity-H").is_none());
}
#[test]
fn the_produced_cids_are_the_repertoire_the_encoding_addresses() {
let map = CodeCidMap::parse(MIXED.as_bytes(), &|_| None).expect("parse");
let mut cids = BTreeSet::new();
map.produced_cids(&mut cids);
assert!(cids.contains(&0) && cids.contains(&1));
assert!((814..=908).all(|cid| cids.contains(&cid)));
assert!(cids.contains(&7000) && cids.contains(&7777));
assert!(!cids.contains(&813) && !cids.contains(&909));
assert!(cids.iter().all(|cid| map.produces(*cid)));
assert_eq!(
cids.len(),
(0..=u32::from(u16::MAX))
.filter(|cid| map.produces(*cid))
.count(),
"the set and the per-CID answer have to agree"
);
}
#[test]
fn overlapping_cid_ranges_merge_into_the_set_they_name() {
const OVERLAPPING: &str = "\
/CIDInit /ProcSet findresource begin
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
4 begincidrange
<0000> <0009> 100
<0005> <000e> 105
<0100> <0100> 200
<0101> <0101> 201
endcidrange
endcmap
";
let map = CodeCidMap::parse(OVERLAPPING.as_bytes(), &|_| None).expect("parse");
let mut cids = BTreeSet::new();
map.produced_cids(&mut cids);
assert_eq!(
cids.iter().copied().collect::<Vec<_>>(),
[0].into_iter()
.chain(100..=114)
.chain([200, 201])
.collect::<Vec<_>>()
);
}
#[test]
fn a_repeated_whole_space_range_is_not_paid_for_once_per_entry() {
let mut cmap = String::from(
"/CIDInit /ProcSet findresource begin\n\
1 begincodespacerange\n <0000> <FFFF>\nendcodespacerange\n",
);
const REPEATS: usize = 1_000;
for chunk in 0..REPEATS / 100 {
cmap.push_str("100 begincidrange\n");
for _ in 0..100 {
cmap.push_str("<0000> <ffff> 0\n");
}
cmap.push_str("endcidrange\n");
let _ = chunk;
}
cmap.push_str("endcmap\n");
let map = CodeCidMap::parse(cmap.as_bytes(), &|_| None).expect("parse");
let started = std::time::Instant::now();
let mut cids = BTreeSet::new();
map.produced_cids(&mut cids);
let took = started.elapsed();
assert_eq!(cids.len(), 1 << 16, "the whole CID space, counted once");
assert!(
took < std::time::Duration::from_secs(10),
"{REPEATS} copies of one range took {took:?}: the ranges are being \
expanded one at a time"
);
}
#[test]
fn a_predefined_table_produces_far_fewer_cids_than_identity_reaches() {
let table = predefined("UniJIS-UTF16-H").expect("the table is compiled in");
let mut cids = BTreeSet::new();
table.produced_cids(&mut cids);
assert!(
(1000..40000).contains(&cids.len()),
"UniJIS-UTF16-H produces the CIDs of Adobe-Japan1, not 65,536 of them: {}",
cids.len()
);
assert!(cids.iter().all(|cid| *cid <= u32::from(u16::MAX)));
}
}