const TABLES: &[u8] = include_bytes!("ctext.bin");
const SETS96: [(u8, &str); 14] = [
(b'B', "iso8859-2"),
(b'C', "iso8859-3"),
(b'D', "iso8859-4"),
(b'L', "iso8859-5"),
(b'G', "iso8859-6"),
(b'F', "iso8859-7"),
(b'H', "iso8859-8"),
(b'M', "iso8859-9"),
(b'V', "iso8859-10"),
(b'T', "iso8859-11"),
(b'Y', "iso8859-13"),
(b'_', "iso8859-14"),
(b'b', "iso8859-15"),
(b'f', "iso8859-16"),
];
const SETS94N: [u8; 3] = *b"ABC";
#[derive(Clone, Copy, Debug, PartialEq)]
enum Set {
Ascii,
Latin1,
JisRoman,
JisKana,
Iso8859(usize),
Double(usize),
Unknown {
double: bool,
},
}
fn table_u16(index: usize) -> u16 {
let at = index * 2;
TABLES
.get(at..at + 2)
.map_or(0, |b| u16::from_le_bytes([b[0], b[1]]))
}
fn single(set: Set, byte: u8) -> char {
let b = byte & 0x7f;
match set {
Set::Ascii => b as char,
Set::Latin1 => char::from(b | 0x80),
Set::JisRoman => match b {
0x5c => '¥',
0x7e => '‾',
_ => b as char,
},
Set::JisKana => match b {
0x21..=0x5f => char::from_u32(0xff61 + (b as u32 - 0x21)).unwrap_or('\u{fffd}'),
_ => '\u{fffd}',
},
Set::Iso8859(i) => {
let v = table_u16(i * 96 + (b as usize).saturating_sub(0x20));
char::from_u32(v as u32)
.filter(|_| v != 0)
.unwrap_or('\u{fffd}')
}
Set::Double(_) | Set::Unknown { .. } => '\u{fffd}',
}
}
fn double(i: usize, b1: u8, b2: u8) -> char {
let (r, c) = ((b1 & 0x7f) as usize, (b2 & 0x7f) as usize);
if !(0x21..0x7f).contains(&r) || !(0x21..0x7f).contains(&c) {
return '\u{fffd}';
}
let v = table_u16(SETS96.len() * 96 + i * 94 * 94 + (r - 0x21) * 94 + (c - 0x21));
char::from_u32(v as u32)
.filter(|_| v != 0)
.unwrap_or('\u{fffd}')
}
pub(super) fn decode(bytes: &[u8]) -> String {
let mut out = String::new();
let (mut gl, mut gr) = (Set::Ascii, Set::Latin1);
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
match b {
0x1b => {
i += 1;
i = escape(bytes, i, &mut gl, &mut gr, &mut out);
}
0x9b => {
i += 1;
while i < bytes.len() && !(0x40..=0x7e).contains(&bytes[i]) {
i += 1;
}
i += 1;
}
b'\n' | b'\t' => {
out.push(b as char);
i += 1;
}
0x20..=0x7f | 0xa0..=0xff => {
let set = if b < 0x80 { gl } else { gr };
if b == 0x20 || b == 0x7f {
out.push(b as char);
i += 1;
} else if let Set::Double(t) = set {
out.push(match bytes.get(i + 1) {
Some(&b2) => double(t, b, b2),
None => '\u{fffd}',
});
i += 2;
} else if let Set::Unknown { double: true } = set {
out.push('\u{fffd}');
i += 2;
} else {
out.push(single(set, b));
i += 1;
}
}
_ => i += 1, }
}
out
}
fn escape(bytes: &[u8], i: usize, gl: &mut Set, gr: &mut Set, out: &mut String) -> usize {
let at = |k: usize| bytes.get(k).copied().unwrap_or(0);
match (at(i), at(i + 1), at(i + 2)) {
(b'(', b'B', _) => *gl = Set::Ascii,
(b'(', b'J', _) => *gl = Set::JisRoman,
(b'(', b'I', _) => *gl = Set::JisKana,
(b')', b'I', _) => *gr = Set::JisKana,
(b'(' | b')', _, _) => {
let set = Set::Unknown { double: false };
if at(i) == b'(' {
*gl = set;
} else {
*gr = set;
}
}
(b'-', b'A', _) => *gr = Set::Latin1,
(b'-', f, _) => {
*gr = SETS96
.iter()
.position(|&(fin, _)| fin == f)
.map_or(Set::Unknown { double: false }, Set::Iso8859);
}
(b'$', side @ (b'(' | b')'), f) => {
let set = SETS94N
.iter()
.position(|&fin| fin == f)
.map_or(Set::Unknown { double: true }, Set::Double);
if side == b'(' {
*gl = set;
} else {
*gr = set;
}
return i + 3;
}
(b'$', f @ (b'A' | b'B'), _) => {
*gl = Set::Double(SETS94N.iter().position(|&fin| fin == f).unwrap_or(0));
}
(b'%', b'G', _) => {
let start = i + 2;
let end = bytes[start..]
.windows(3)
.position(|w| w == b"\x1b%@")
.map_or(bytes.len(), |p| start + p);
out.push_str(&String::from_utf8_lossy(&bytes[start..end]));
return (end + 3).min(bytes.len());
}
(b'%', b'/', _) => {
let (m, l) = (at(i + 3), at(i + 4));
let len = ((m & 0x7f) as usize) * 128 + (l & 0x7f) as usize;
let start = (i + 5).min(bytes.len());
let end = (start + len).min(bytes.len());
let seg = &bytes[start..end];
let (name, data) = match seg.iter().position(|&c| c == 0x02) {
Some(p) => (&seg[..p], &seg[p + 1..]),
None => (seg, &[][..]),
};
let name = String::from_utf8_lossy(name).to_ascii_lowercase();
if name == "iso10646-1" || name == "utf-8" {
out.push_str(&String::from_utf8_lossy(data));
} else if let Some(t) = SETS96.iter().position(|&(_, n)| n == name) {
for &c in data {
out.push(if c < 0x80 {
c as char
} else {
single(Set::Iso8859(t), c)
});
}
} else {
out.extend(data.iter().map(|_| '\u{fffd}'));
}
return end;
}
_ => {}
}
(i + 2).min(bytes.len())
}
#[cfg(test)]
mod tests {
use super::decode;
#[test]
fn ascii_and_latin1_need_no_escapes() {
assert_eq!(decode(b"abc \xe9\xa9"), "abc é©");
}
#[test]
fn utf8_segments() {
assert_eq!(decode("\x1b%G中文\x1b%@!".as_bytes()), "中文!");
assert_eq!(decode("\x1b%Gあ".as_bytes()), "あ");
}
#[test]
fn double_byte_sets_in_gl_and_gr() {
assert_eq!(decode(b"\x1b$(BCf\x1b(B!"), "中!");
assert_eq!(decode(b"\x1b$)A\xd6\xd0"), "中");
assert_eq!(decode(b"\x1b$(CGQ"), "한");
assert_eq!(decode(b"\x1b$(B$\""), "あ");
}
#[test]
fn iso8859_parts_and_kana() {
assert_eq!(decode(b"\x1b-L\xd0\x1b-F\xe1"), "аα");
assert_eq!(decode(b"\x1b-L\xd0\x1b-A\xe9"), "аé");
assert_eq!(decode(b"\x1b)I\xb1"), "ア");
}
#[test]
fn extended_segments() {
assert_eq!(decode(b"\x1b%/1\x80\x8ciso8859-15\x02\xa4"), "€");
assert_eq!(
decode(b"\x1b%/2\x80\x89big5-0\x02\xa4@x"),
"\u{fffd}\u{fffd}x"
);
}
#[test]
fn malformed_input_does_not_panic() {
for bad in [&b"\x1b"[..], b"\x1b$", b"\x1b$(B\x30", b"\x9b1", b"\x1b%/"] {
let _ = decode(bad);
}
}
}