use crate::parser::read_u16;
use crate::Error;
pub mod name_id {
pub const COPYRIGHT: u16 = 0;
pub const FAMILY: u16 = 1;
pub const SUBFAMILY: u16 = 2;
pub const UNIQUE_ID: u16 = 3;
pub const FULL_NAME: u16 = 4;
pub const VERSION: u16 = 5;
pub const POSTSCRIPT: u16 = 6;
pub const TRADEMARK: u16 = 7;
pub const MANUFACTURER: u16 = 8;
pub const DESIGNER: u16 = 9;
pub const DESCRIPTION: u16 = 10;
pub const VENDOR_URL: u16 = 11;
pub const DESIGNER_URL: u16 = 12;
pub const LICENSE: u16 = 13;
pub const LICENSE_URL: u16 = 14;
pub const TYPOGRAPHIC_FAMILY: u16 = 16;
pub const TYPOGRAPHIC_SUBFAMILY: u16 = 17;
pub const COMPATIBLE_FULL: u16 = 18;
pub const SAMPLE_TEXT: u16 = 19;
pub const POSTSCRIPT_CID: u16 = 20;
}
pub mod platform {
pub const UNICODE: u16 = 0;
pub const MACINTOSH: u16 = 1;
pub const WINDOWS: u16 = 3;
}
#[derive(Debug, Clone)]
pub struct NameRecord {
pub platform_id: u16,
pub encoding_id: u16,
pub language_id: u16,
pub name_id: u16,
pub string: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NameTable<'a> {
bytes: &'a [u8],
count: u16,
string_offset: u16,
_phantom: core::marker::PhantomData<&'a ()>,
}
impl<'a> NameTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < 6 {
return Err(Error::UnexpectedEof);
}
let format = read_u16(bytes, 0)?;
if format > 1 {
return Err(Error::BadStructure("name.format > 1"));
}
let count = read_u16(bytes, 2)?;
let string_offset = read_u16(bytes, 4)?;
let table_end = 6usize + count as usize * 12;
if bytes.len() < table_end {
return Err(Error::UnexpectedEof);
}
if (string_offset as usize) > bytes.len() {
return Err(Error::BadOffset);
}
Ok(Self {
bytes,
count,
string_offset,
_phantom: core::marker::PhantomData,
})
}
pub fn len(&self) -> usize {
self.count as usize
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
fn record_header(&self, i: usize) -> Option<(u16, u16, u16, u16, usize, usize)> {
if i >= self.count as usize {
return None;
}
let off = 6 + i * 12;
let platform = read_u16(self.bytes, off).ok()?;
let encoding = read_u16(self.bytes, off + 2).ok()?;
let language = read_u16(self.bytes, off + 4).ok()?;
let nid = read_u16(self.bytes, off + 6).ok()?;
let length = read_u16(self.bytes, off + 8).ok()? as usize;
let str_off = read_u16(self.bytes, off + 10).ok()? as usize;
Some((platform, encoding, language, nid, length, str_off))
}
pub fn record_bytes(&self, i: usize) -> Option<&'a [u8]> {
let (_, _, _, _, length, str_off) = self.record_header(i)?;
let start = self.string_offset as usize + str_off;
let end = start.checked_add(length)?;
self.bytes.get(start..end)
}
pub fn records(&self) -> Vec<NameRecord> {
let mut out = Vec::with_capacity(self.count as usize);
for i in 0..self.count as usize {
let Some((platform, encoding, language, name_id, _, _)) = self.record_header(i) else {
continue;
};
let string = self
.record_bytes(i)
.and_then(|raw| decode(platform, encoding, raw).map(|c| c.into_owned()));
out.push(NameRecord {
platform_id: platform,
encoding_id: encoding,
language_id: language,
name_id,
string,
});
}
out
}
pub fn find(&self, name_id: u16) -> Option<&'a str> {
let mut best: Option<(i32, std::borrow::Cow<'a, str>)> = None;
for i in 0..self.count as usize {
let (platform, encoding, language, nid, length, str_off) = match self.record_header(i) {
Some(h) => h,
None => continue,
};
if nid != name_id {
continue;
}
let start = self.string_offset as usize + str_off;
let end = start.checked_add(length)?;
let raw = self.bytes.get(start..end)?;
let rank = rank_record(platform, encoding, language);
let decoded = match decode(platform, encoding, raw) {
Some(d) => d,
None => continue,
};
match &best {
Some((br, _)) if *br >= rank => {}
_ => best = Some((rank, decoded)),
}
}
let (_, c) = best?;
Some(match c {
std::borrow::Cow::Borrowed(s) => s,
std::borrow::Cow::Owned(s) => Box::leak(s.into_boxed_str()),
})
}
pub fn find_for(&self, name_id: u16, platform_id: u16, language_id: u16) -> Option<String> {
for i in 0..self.count as usize {
let (platform, encoding, language, nid, length, str_off) = self.record_header(i)?;
if nid != name_id || platform != platform_id || language != language_id {
continue;
}
let start = self.string_offset as usize + str_off;
let end = start.checked_add(length)?;
let raw = self.bytes.get(start..end)?;
if let Some(decoded) = decode(platform, encoding, raw) {
return Some(decoded.into_owned());
}
}
None
}
}
fn rank_record(platform: u16, encoding: u16, language: u16) -> i32 {
match (platform, encoding, language) {
(3, 1, 0x0409) => 100, (3, 1, l) if l & 0xFF == 9 => 90, (3, 1, _) => 80,
(3, 10, _) => 75, (1, 0, 0) => 70, (0, _, _) => 60, _ => 10,
}
}
fn decode<'a>(platform: u16, encoding: u16, raw: &'a [u8]) -> Option<std::borrow::Cow<'a, str>> {
match (platform, encoding) {
(0, _) | (3, 1) | (3, 10) => {
if raw.len() % 2 != 0 {
return None;
}
let mut s = String::with_capacity(raw.len() / 2);
let mut i = 0;
while i + 1 < raw.len() {
let u = u16::from_be_bytes([raw[i], raw[i + 1]]);
i += 2;
if (0xD800..=0xDBFF).contains(&u) {
if i + 1 >= raw.len() {
return None;
}
let lo = u16::from_be_bytes([raw[i], raw[i + 1]]);
if !(0xDC00..=0xDFFF).contains(&lo) {
return None;
}
i += 2;
let cp = 0x10000 + (((u - 0xD800) as u32) << 10) + (lo - 0xDC00) as u32;
s.push(char::from_u32(cp)?);
} else {
s.push(char::from_u32(u as u32)?);
}
}
Some(std::borrow::Cow::Owned(s))
}
(1, 0) => {
if raw.iter().all(|&b| b < 0x80) {
std::str::from_utf8(raw)
.ok()
.map(std::borrow::Cow::Borrowed)
} else {
Some(std::borrow::Cow::Owned(
raw.iter()
.map(|&b| if b < 0x80 { b as char } else { '?' })
.collect(),
))
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_minimal() -> Vec<u8> {
let utf16: Vec<u8> = "Hi".encode_utf16().flat_map(|u| u.to_be_bytes()).collect();
let length = utf16.len() as u16;
let header_size = 6 + 12;
let mut out = vec![0u8; header_size];
out[0..2].copy_from_slice(&0u16.to_be_bytes()); out[2..4].copy_from_slice(&1u16.to_be_bytes()); out[4..6].copy_from_slice(&(header_size as u16).to_be_bytes()); out[6..8].copy_from_slice(&3u16.to_be_bytes()); out[8..10].copy_from_slice(&1u16.to_be_bytes()); out[10..12].copy_from_slice(&0x0409u16.to_be_bytes()); out[12..14].copy_from_slice(&1u16.to_be_bytes()); out[14..16].copy_from_slice(&length.to_be_bytes()); out[16..18].copy_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&utf16);
out
}
fn build_multi(records: &[(u16, u16, u16, u16, &[u8])]) -> Vec<u8> {
let header_size = 6 + records.len() * 12;
let mut out = vec![0u8; header_size];
out[0..2].copy_from_slice(&0u16.to_be_bytes()); out[2..4].copy_from_slice(&(records.len() as u16).to_be_bytes());
out[4..6].copy_from_slice(&(header_size as u16).to_be_bytes()); let mut storage: Vec<u8> = Vec::new();
for (i, &(p, e, l, n, raw)) in records.iter().enumerate() {
let off = 6 + i * 12;
out[off..off + 2].copy_from_slice(&p.to_be_bytes());
out[off + 2..off + 4].copy_from_slice(&e.to_be_bytes());
out[off + 4..off + 6].copy_from_slice(&l.to_be_bytes());
out[off + 6..off + 8].copy_from_slice(&n.to_be_bytes());
out[off + 8..off + 10].copy_from_slice(&(raw.len() as u16).to_be_bytes());
out[off + 10..off + 12].copy_from_slice(&(storage.len() as u16).to_be_bytes());
storage.extend_from_slice(raw);
}
out.extend_from_slice(&storage);
out
}
fn utf16be(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(|u| u.to_be_bytes()).collect()
}
#[test]
fn decodes_utf16_be() {
let bytes = build_minimal();
let n = NameTable::parse(&bytes).unwrap();
assert_eq!(n.find(1), Some("Hi"));
assert_eq!(n.find(99), None);
}
#[test]
fn well_known_name_id_constants_match_tn5149() {
assert_eq!(name_id::COPYRIGHT, 0);
assert_eq!(name_id::FAMILY, 1);
assert_eq!(name_id::SUBFAMILY, 2);
assert_eq!(name_id::FULL_NAME, 4);
assert_eq!(name_id::VERSION, 5);
assert_eq!(name_id::POSTSCRIPT, 6);
assert_eq!(name_id::LICENSE_URL, 14);
assert_eq!(name_id::TYPOGRAPHIC_FAMILY, 16);
assert_eq!(name_id::POSTSCRIPT_CID, 20);
}
#[test]
fn records_enumerates_every_record_with_locator_tuple() {
let fam = utf16be("Acme Sans");
let ver = utf16be("Version 1.0");
let bytes = build_multi(&[
(3, 1, 0x0409, name_id::FAMILY, &fam),
(3, 1, 0x0409, name_id::VERSION, &ver),
]);
let n = NameTable::parse(&bytes).unwrap();
assert_eq!(n.len(), 2);
assert!(!n.is_empty());
let recs = n.records();
assert_eq!(recs.len(), 2);
assert_eq!(recs[0].platform_id, platform::WINDOWS);
assert_eq!(recs[0].encoding_id, 1);
assert_eq!(recs[0].language_id, 0x0409);
assert_eq!(recs[0].name_id, name_id::FAMILY);
assert_eq!(recs[0].string.as_deref(), Some("Acme Sans"));
assert_eq!(recs[1].name_id, name_id::VERSION);
assert_eq!(recs[1].string.as_deref(), Some("Version 1.0"));
}
#[test]
fn find_for_targets_exact_locale_without_ranking() {
let en = utf16be("Acme Sans");
let ja = utf16be("\u{30A2}\u{30AF}\u{30E1}"); let bytes = build_multi(&[
(3, 1, 0x0411, name_id::FAMILY, &ja), (3, 1, 0x0409, name_id::FAMILY, &en),
]);
let n = NameTable::parse(&bytes).unwrap();
assert_eq!(n.find(name_id::FAMILY), Some("Acme Sans"));
assert_eq!(
n.find_for(name_id::FAMILY, platform::WINDOWS, 0x0411)
.as_deref(),
Some("\u{30A2}\u{30AF}\u{30E1}")
);
assert_eq!(
n.find_for(name_id::FAMILY, platform::WINDOWS, 0x0409)
.as_deref(),
Some("Acme Sans")
);
assert_eq!(n.find_for(name_id::FAMILY, platform::WINDOWS, 0x0407), None);
assert_eq!(
n.find_for(name_id::VERSION, platform::WINDOWS, 0x0409),
None
);
}
#[test]
fn mac_nonroman_record_undecodable_but_locator_and_bytes_surfaced() {
let mac_bytes = [0x82u8, 0xA0, 0x82, 0xA2]; let bytes = build_multi(&[(1, 1, 11, name_id::FAMILY, &mac_bytes)]);
let n = NameTable::parse(&bytes).unwrap();
let recs = n.records();
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].platform_id, platform::MACINTOSH);
assert_eq!(recs[0].encoding_id, 1); assert_eq!(recs[0].language_id, 11);
assert!(recs[0].string.is_none());
assert_eq!(n.record_bytes(0), Some(&mac_bytes[..]));
assert_eq!(n.record_bytes(1), None);
}
#[test]
fn mac_roman_ascii_decodes() {
let bytes = build_multi(&[(1, 0, 0, name_id::FULL_NAME, b"Acme Sans Bold")]);
let n = NameTable::parse(&bytes).unwrap();
assert_eq!(
n.find_for(name_id::FULL_NAME, platform::MACINTOSH, 0)
.as_deref(),
Some("Acme Sans Bold")
);
}
}