use crate::blob::{self, Entry};
const MAX_CHAIN: usize = 8;
fn chain(reg: usize, index: usize) -> impl Iterator<Item = Entry> {
let mut at = Some(index);
let mut links = 0usize;
std::iter::from_fn(move || {
let i = at?;
let e = blob::entry(reg, i)?;
links += 1;
at = if e.use_offset == 0 || links >= MAX_CHAIN {
None
} else {
isize::try_from(i)
.ok()
.and_then(|i| i.checked_add(isize::from(e.use_offset)))
.and_then(|n| usize::try_from(n).ok())
};
Some(e)
})
}
fn lower_bound(entry: &Entry, needle: u16) -> usize {
let mut lo = 0usize;
let mut len = usize::from(entry.word_count);
while len > 0 {
let half = len / 2;
let mid = lo + half;
let key = blob::word_record(entry, mid).map_or(u16::MAX, |(_, high, _)| high);
if key < needle {
lo = mid + 1;
len -= half + 1;
} else {
len = half;
}
}
lo
}
fn lower_bound_dword(entry: &Entry, hi: u16, lo: u16) -> usize {
let mut base = 0usize;
let mut len = usize::from(entry.dword_count);
while len > 0 {
let half = len / 2;
let mid = base + half;
let less = blob::dword_record(entry, mid).is_some_and(|(rec_hi, _, rec_lo_high, _)| {
if rec_hi == hi {
rec_lo_high < lo
} else {
rec_hi < hi
}
});
if less {
base = mid + 1;
len -= half + 1;
} else {
len = half;
}
}
base
}
pub(crate) fn cid_from_charcode(reg: usize, index: usize, charcode: u32) -> u16 {
let hi = (charcode >> 16) as u16;
let lo = charcode as u16;
if hi != 0 {
return cid_from_dword(reg, index, hi, lo);
}
for entry in chain(reg, index) {
let at = lower_bound(&entry, lo);
if let Some((low, high, cid)) = blob::word_record(&entry, at)
&& lo >= low
&& lo <= high
{
return (u32::from(cid) + u32::from(lo) - u32::from(low)) as u16;
}
}
0
}
fn cid_from_dword(reg: usize, index: usize, hi: u16, lo: u16) -> u16 {
for entry in chain(reg, index) {
if entry.dword_count == 0 {
continue;
}
let at = lower_bound_dword(&entry, hi, lo);
if let Some((_, lo_low, lo_high, cid)) = blob::dword_record(&entry, at)
&& lo >= lo_low
&& lo <= lo_high
{
return (u32::from(cid) + u32::from(lo) - u32::from(lo_low)) as u16;
}
}
0
}
pub(crate) fn charcode_from_cid(reg: usize, index: usize, cid: u16) -> u32 {
for entry in chain(reg, index) {
for r in 0..usize::from(entry.word_count) {
let Some((low, high, rec_cid)) = blob::word_record(&entry, r) else {
break;
};
let span = u32::from(rec_cid) + u32::from(high) - u32::from(low);
if u32::from(cid) >= u32::from(rec_cid) && u32::from(cid) <= span {
return u32::from(low) + u32::from(cid) - u32::from(rec_cid);
}
}
}
0
}
pub(crate) fn find(reg: usize, name: &[u8]) -> Option<usize> {
(0..blob::entry_count(reg))
.find(|&i| blob::entry(reg, i).and_then(|e| blob::name(&e)) == Some(name))
}
#[cfg(test)]
mod tests {
use super::{charcode_from_cid, cid_from_charcode, find};
const GB1: usize = 0;
const CNS1: usize = 1;
const JAPAN1: usize = 2;
const KOREA1: usize = 3;
fn at(reg: usize, name: &[u8]) -> usize {
find(reg, name).expect("name is in the blob")
}
#[test]
fn gb_euc_h_spot_lookups() {
let i = at(GB1, b"GB-EUC-H");
assert_eq!(cid_from_charcode(GB1, i, 0x0020), 0x1E24);
assert_eq!(cid_from_charcode(GB1, i, 0x0021), 0x032E);
assert_eq!(cid_from_charcode(GB1, i, 0x007E), 0x032E + 0x7E - 0x21);
assert_eq!(cid_from_charcode(GB1, i, 0x0060), 0x032E + 0x60 - 0x21);
assert_eq!(cid_from_charcode(GB1, i, 0xA1A1), 0x0060);
assert_eq!(cid_from_charcode(GB1, i, 0xA1FE), 0x0060 + 0x5D);
assert_eq!(cid_from_charcode(GB1, i, 0x0000), 0);
assert_eq!(cid_from_charcode(GB1, i, 0x001F), 0);
assert_eq!(cid_from_charcode(GB1, i, 0x00FF), 0);
}
#[test]
fn vertical_chains_back_to_horizontal() {
let (v, h) = (at(GB1, b"GB-EUC-V"), at(GB1, b"GB-EUC-H"));
assert_eq!(v, h + 1);
assert_eq!(
cid_from_charcode(GB1, v, 0xA1A1),
cid_from_charcode(GB1, h, 0xA1A1)
);
let differs =
(0u32..=0xFFFF).any(|c| cid_from_charcode(GB1, v, c) != cid_from_charcode(GB1, h, c));
assert!(differs, "GB-EUC-V must override something");
}
#[test]
fn korea1_six_row_hop() {
let (pc, ksc) = (at(KOREA1, b"KSCpc-EUC-H"), at(KOREA1, b"KSC-EUC-H"));
assert_eq!(pc, ksc + 6);
assert!(find(KOREA1, b"KSCpc-EUC-V").is_none());
let borrowed = (0u32..=0xFFFF).find(|&c| {
cid_from_charcode(KOREA1, pc, c) != 0
&& cid_from_charcode(KOREA1, pc, c) == cid_from_charcode(KOREA1, ksc, c)
});
assert!(borrowed.is_some(), "the -6 hop must reach KSC-EUC-H");
}
#[test]
fn chain_crosses_from_range_to_single() {
let (v, h) = (at(CNS1, b"UniCNS-UTF16-V"), at(CNS1, b"UniCNS-UTF16-H"));
assert_eq!(v, h + 1);
assert!(crate::blob::entry(CNS1, v).unwrap().is_range);
assert!(!crate::blob::entry(CNS1, h).unwrap().is_range);
let reached = (0u32..=0xFFFF).find(|&c| {
cid_from_charcode(CNS1, v, c) == cid_from_charcode(CNS1, h, c)
&& cid_from_charcode(CNS1, h, c) != 0
});
assert!(reached.is_some(), "V must reach H's single table");
}
#[test]
fn japan1_hw_chain_crosses_type() {
let (hw, base) = (
at(JAPAN1, b"UniJIS-UCS2-HW-H"),
at(JAPAN1, b"UniJIS-UCS2-H"),
);
assert_eq!(hw, base + 2);
assert_eq!(crate::blob::entry(JAPAN1, hw).unwrap().word_count, 4);
assert!(!crate::blob::entry(JAPAN1, base).unwrap().is_range);
let reached = (0u32..=0xFFFF).find(|&c| {
cid_from_charcode(JAPAN1, hw, c) == cid_from_charcode(JAPAN1, base, c)
&& cid_from_charcode(JAPAN1, base, c) != 0
});
assert!(reached.is_some());
}
#[test]
fn dword_path_resolves_four_byte_codes() {
let i = at(GB1, b"GBK2K-H");
assert_eq!(cid_from_charcode(GB1, i, 0x8130_8436), 0x5752);
assert_eq!(cid_from_charcode(GB1, i, 0x8138_FD38), 0x579C);
assert_eq!(cid_from_charcode(GB1, i, 0x8138_FD39), 0x579D);
}
#[test]
fn dword_path_never_falls_back_to_the_word_tables() {
let i = at(GB1, b"GB-EUC-H");
assert_eq!(cid_from_charcode(GB1, i, 0x0001_A1A1), 0);
assert_ne!(cid_from_charcode(GB1, i, 0xA1A1), 0);
}
#[test]
fn reverse_lookup_round_trips_the_spot_codes() {
let i = at(GB1, b"GB-EUC-H");
for code in [0x0020u32, 0x0021, 0x007E, 0xA1A1, 0xA1FE] {
let cid = cid_from_charcode(GB1, i, code);
assert_ne!(cid, 0, "code {code:#x} must map");
assert_eq!(charcode_from_cid(GB1, i, cid), code, "round trip {code:#x}");
}
assert_eq!(charcode_from_cid(GB1, i, 0), 0);
}
#[test]
fn reverse_lookup_ignores_dword_tables() {
let i = at(GB1, b"GBK2K-H");
let cid = cid_from_charcode(GB1, i, 0x8130_8436);
assert_eq!(cid, 0x5752);
assert_ne!(charcode_from_cid(GB1, i, cid), 0x8130_8436);
}
#[test]
fn unknown_names_and_indices_are_absent() {
assert!(find(GB1, b"GB-EUC").is_none());
assert!(find(GB1, b"").is_none());
assert!(find(GB1, b"Identity-H").is_none());
assert_eq!(cid_from_charcode(GB1, 999, 0x20), 0);
assert_eq!(charcode_from_cid(GB1, 999, 1), 0);
}
}