use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use fusevm::{Op, Value, VM};
use crate::compiler::{CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{place_at, to_tcl_string, var_cell};
pub mod ext {
pub use crate::compiler::ext::ENCODING_BASE as BASE;
pub const CONVERT: u16 = BASE;
pub const NAMES: u16 = BASE + 1;
pub const SYSTEM: u16 = BASE + 2;
pub const DIRS: u16 = BASE + 3;
pub const PROFILES: u16 = BASE + 4;
pub const USER: u16 = BASE + 5;
}
pub const COMMANDS: &[&str] = &["encoding"];
pub const SUBCOMMANDS: &[&str] = &[
"convertfrom",
"convertto",
"dirs",
"names",
"profiles",
"system",
"user",
];
const PROFILES: &[&str] = &["replace", "strict", "tcl8"];
const ARG_TO: u8 = 1;
const ARG_PROFILE: u8 = 2;
const ARG_FAILINDEX: u8 = 4;
const ARG_SLOT: u8 = 8;
const ARG_SYSTEM: u8 = 16;
pub(crate) fn compile(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let Some(first) = args.first() else {
return c.error("wrong # args: should be \"encoding subcommand ?arg ...?\"");
};
let given = c.literal_of(first, "subcommand")?.to_string();
let Some(sub) = resolve(&given, SUBCOMMANDS) else {
return c.error(format!(
"unknown or ambiguous subcommand \"{given}\": must be {}",
listing(SUBCOMMANDS)
));
};
let rest = &args[1..];
match sub {
"convertfrom" | "convertto" => compile_convert(c, sub, rest),
"names" | "profiles" => {
if !rest.is_empty() {
return c.error(format!("wrong # args: should be \"encoding {sub}\""));
}
let id = if sub == "names" {
ext::NAMES
} else {
ext::PROFILES
};
c.emit(Op::Extended(id, 0), 1);
Ok(())
}
"user" => {
if !rest.is_empty() {
return c.error("wrong # args: should be \"encoding user \"");
}
c.push_str("");
c.emit(Op::Extended(ext::USER, 0), 0);
Ok(())
}
other => {
if rest.len() > 1 {
let usage = if other == "system" {
"?encoding?"
} else {
"?dirList?"
};
return c.error(format!(
"wrong # args: should be \"encoding {other} {usage}\""
));
}
let given = match rest.first() {
Some(w) => {
c.word(w)?;
1
}
None => {
c.push_str("");
0
}
};
let id = if other == "system" {
ext::SYSTEM
} else {
ext::DIRS
};
c.emit(Op::Extended(id, given), 0);
Ok(())
}
}
}
fn compile_convert(c: &mut Compiler, sub: &str, args: &[Word]) -> Result<(), CompileError> {
let n = args.len();
if n == 0 {
return c.error(wrong_args(sub, n));
}
let mut flags = if sub == "convertto" { ARG_TO } else { 0 };
let mut profile: Option<&Word> = None;
let mut failindex: Option<&Word> = None;
if n == 1 {
flags |= ARG_SYSTEM;
} else {
let mut k = 0;
while k + 2 < n {
let name = c.literal_of(&args[k], "option")?.to_string();
let Some(option) = resolve(&name, &["-profile", "-failindex"]) else {
return runtime_error(c, bad_option(&name));
};
k += 1;
if k == n - 2 {
return c.error(wrong_args(sub, n));
}
match option {
"-profile" => {
profile = Some(&args[k]);
flags |= ARG_PROFILE;
}
_ => {
failindex = Some(&args[k]);
flags |= ARG_FAILINDEX;
}
}
k += 1;
}
}
match profile {
Some(w) => c.word(w)?,
None => c.push_str(""),
}
match failindex {
Some(w) => {
let name = c.var_name_of(w)?;
let encoded = c.place_operand(&name);
if encoded & 1 == 1 {
flags |= ARG_SLOT;
}
c.emit(Op::LoadInt(encoded >> 1), 1);
}
None => {
c.emit(Op::LoadInt(-1), 1);
}
}
if flags & ARG_SYSTEM != 0 {
c.push_str("");
c.word(&args[0])?;
} else {
c.word(&args[n - 2])?;
c.word(&args[n - 1])?;
}
c.emit(Op::Extended(ext::CONVERT, flags), -3);
Ok(())
}
fn wrong_args(sub: &str, argc: usize) -> String {
let name = if (1..=3).contains(&argc) {
format!("::tcl::encoding::{sub}")
} else {
format!("encoding {sub}")
};
format!(
"wrong # args: should be \"{name} ?-profile profile? ?-failindex var? encoding data\" \
or \"{name} data\""
)
}
fn bad_option(name: &str) -> String {
let ambiguous = !name.is_empty()
&& ["-profile", "-failindex"]
.iter()
.filter(|o| o.starts_with(name))
.count()
> 1;
let what = if ambiguous { "ambiguous" } else { "bad" };
format!("{what} option \"{name}\": must be -profile or -failindex")
}
fn runtime_error(c: &mut Compiler, message: String) -> Result<(), CompileError> {
c.push_str(&message);
c.emit(Op::Extended(crate::compiler::ext::ERROR, 0), -1);
c.push_empty();
Ok(())
}
fn resolve<'t>(name: &str, table: &[&'t str]) -> Option<&'t str> {
if let Some(exact) = table.iter().find(|c| **c == name) {
return Some(exact);
}
if name.is_empty() {
return None;
}
let mut hit = None;
for candidate in table {
if candidate.starts_with(name) {
if hit.is_some() {
return None;
}
hit = Some(*candidate);
}
}
hit
}
fn listing(table: &[&str]) -> String {
let mut out = String::new();
for (i, name) in table.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if i + 1 == table.len() {
out.push_str("or ");
}
out.push_str(name);
}
out
}
pub(crate) fn is_op(id: u16) -> bool {
(ext::BASE..ext::BASE + crate::compiler::ext::BLOCK).contains(&id)
}
fn state() -> &'static Mutex<(String, String)> {
static STATE: OnceLock<Mutex<(String, String)>> = OnceLock::new();
STATE.get_or_init(|| Mutex::new(("utf-8".to_string(), String::new())))
}
pub(crate) fn extension(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::CONVERT => convert(vm, arg),
ext::NAMES => {
let list = crate::list::join(&names());
vm.push(Value::Str(Arc::new(list)));
Ok(())
}
ext::PROFILES => {
vm.push(Value::Str(Arc::new(crate::list::join(PROFILES))));
Ok(())
}
ext::USER | ext::SYSTEM => {
let given = to_tcl_string(&vm.pop());
if id == ext::SYSTEM && arg == 1 {
if !given.is_empty() {
lookup(&given)?;
}
state().lock().expect("encoding state").0 = given.clone();
vm.push(Value::Str(Arc::new(given)));
return Ok(());
}
let current = state().lock().expect("encoding state").0.clone();
vm.push(Value::Str(Arc::new(current)));
Ok(())
}
ext::DIRS => {
let given = to_tcl_string(&vm.pop());
if arg == 1 {
if crate::list::split(&given).is_err() {
return Err(format!("expected directory list but got \"{given}\""));
}
state().lock().expect("encoding state").1 = given.clone();
vm.push(Value::Str(Arc::new(given)));
return Ok(());
}
let current = state().lock().expect("encoding state").1.clone();
vm.push(Value::Str(Arc::new(current)));
Ok(())
}
other => Err(format!("unknown encoding op {other}")),
}
}
fn convert(vm: &mut VM, flags: u8) -> Result<(), String> {
let data = vm.pop();
let name = to_tcl_string(&vm.pop());
let place = vm.pop();
let profile_word = to_tcl_string(&vm.pop());
let profile = if flags & ARG_PROFILE != 0 {
match PROFILES.iter().position(|p| *p == profile_word) {
Some(0) => Profile::Replace,
Some(1) => Profile::Strict,
Some(2) => Profile::Tcl8,
_ => {
return Err(format!(
"bad profile name \"{profile_word}\": must be {}",
listing(PROFILES)
))
}
}
} else {
Profile::Strict
};
let name = if flags & ARG_SYSTEM != 0 {
state().lock().expect("encoding state").0.clone()
} else {
name
};
let encoding = lookup(&name)?;
let (text, stop) = if flags & ARG_TO != 0 {
let source = to_tcl_string(&data);
let (bytes, stop) = from_utf(&encoding, &source, profile);
let text: String = bytes.iter().map(|b| char::from(*b)).collect();
let stop = stop.map(|at| FailedAt {
index: at,
message: {
let chars = source[..at].chars().count();
let cp = source[at..].chars().next().map_or(0, u32::from);
format!("unexpected character at index {chars}: 'U+{cp:06X}'")
},
});
(text, stop)
} else {
let bytes = as_bytes(&to_tcl_string(&data))?;
let (text, stop) = to_utf(&encoding, &bytes, profile)?;
let stop = stop.map(|at| FailedAt {
index: at,
message: format!(
"unexpected byte sequence starting at index {at}: '\\x{:02X}'",
bytes.get(at).copied().unwrap_or(0)
),
});
(text, stop)
};
if flags & ARG_FAILINDEX != 0 {
let index = stop.as_ref().map_or(-1, |f| f.index as i64);
let place = place_at(&place, flags & ARG_SLOT != 0)?;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Int(index);
}
} else if let Some(failed) = stop {
return Err(failed.message);
}
vm.push(Value::Str(Arc::new(text)));
Ok(())
}
struct FailedAt {
index: usize,
message: String,
}
fn as_bytes(text: &str) -> Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(text.len());
for ch in text.chars() {
let cp = u32::from(ch);
if cp > 255 {
return Err(format!(
"expected code point values below 0xff but value at byte offset {} was 0x{cp:x}",
out.len()
));
}
out.push(cp as u8);
}
Ok(out)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Profile {
Tcl8,
Strict,
Replace,
}
const REPLACE_CHAR: u32 = 0xFFFD;
enum Encoding {
Table(&'static Table),
Utf8,
Cesu8,
Utf16 { le: bool },
Ucs2 { le: bool },
Utf32 { le: bool },
}
const NATIVE_LE: bool = cfg!(target_endian = "little");
const BUILTIN: &[&str] = &[
"cesu-8", "ucs-2", "ucs-2be", "ucs-2le", "unicode", "utf-16", "utf-16be", "utf-16le", "utf-32",
"utf-32be", "utf-32le", "utf-8",
];
const ESCAPE: &[&str] = &["iso2022", "iso2022-jp", "iso2022-kr"];
pub fn names() -> Vec<&'static str> {
let mut all: Vec<&'static str> = BUILTIN.to_vec();
all.extend(crate::encoding_tables::TABLES.iter().map(|(name, _)| *name));
all.sort_unstable();
all
}
fn lookup(name: &str) -> Result<Encoding, String> {
match name {
"utf-8" => return Ok(Encoding::Utf8),
"cesu-8" => return Ok(Encoding::Cesu8),
"utf-16" | "unicode" => return Ok(Encoding::Utf16 { le: NATIVE_LE }),
"utf-16le" => return Ok(Encoding::Utf16 { le: true }),
"utf-16be" => return Ok(Encoding::Utf16 { le: false }),
"ucs-2" => return Ok(Encoding::Ucs2 { le: NATIVE_LE }),
"ucs-2le" => return Ok(Encoding::Ucs2 { le: true }),
"ucs-2be" => return Ok(Encoding::Ucs2 { le: false }),
"utf-32" => return Ok(Encoding::Utf32 { le: NATIVE_LE }),
"utf-32le" => return Ok(Encoding::Utf32 { le: true }),
"utf-32be" => return Ok(Encoding::Utf32 { le: false }),
_ => {}
}
if let Some(table) = table(name) {
return Ok(Encoding::Table(table));
}
if ESCAPE.contains(&name) {
return Err(format!(
"encoding: the escape-sequence encoding \"{name}\" is not supported yet; \
it is a state machine rather than a table and is absent from \"encoding names\""
));
}
Err(format!("unknown encoding \"{name}\""))
}
pub(crate) fn static_name(name: &str) -> Option<&'static str> {
names().into_iter().find(|candidate| *candidate == name)
}
const CHANNEL_PROFILE: Profile = Profile::Strict;
pub(crate) const ILLEGAL_SEQUENCE: &str = "invalid or incomplete multibyte or wide character";
pub(crate) fn stream_decode(name: &str, src: &[u8]) -> Result<(String, usize), String> {
let encoding = lookup(name)?;
let whole = src.len() - incomplete_tail(&encoding, src);
let (text, stop) = to_utf(&encoding, &src[..whole], CHANNEL_PROFILE)?;
if stop.is_some() {
return Err(ILLEGAL_SEQUENCE.to_string());
}
Ok((text, whole))
}
pub(crate) fn stream_encode(name: &str, text: &str) -> Result<Vec<u8>, String> {
let encoding = lookup(name)?;
let (bytes, stop) = from_utf(&encoding, text, CHANNEL_PROFILE);
if stop.is_some() {
return Err(ILLEGAL_SEQUENCE.to_string());
}
Ok(bytes)
}
fn incomplete_tail(encoding: &Encoding, src: &[u8]) -> usize {
match encoding {
Encoding::Table(table) => {
let mut at = 0;
while at < src.len() {
let byte = src[at] as usize;
if !table.prefix[byte] {
at += 1;
continue;
}
if at + 1 >= src.len() {
return src.len() - at;
}
at += 2;
}
0
}
Encoding::Utf8 | Encoding::Cesu8 => {
let mut at = 0;
while at < src.len() {
let lead = src[at];
if lead == 0xC0 || lead == 0xC1 {
if at + 1 >= src.len() {
return 1;
}
at += if src[at + 1] == 0x80 { 2 } else { 1 };
continue;
}
if let Some((_, len)) = decode_utf8(&src[at..]) {
at += len;
continue;
}
let need = match lead {
0xC2..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF4 => 4,
_ => 1,
};
let have = src.len() - at;
let cut = have < need && src[at + 1..].iter().all(|byte| byte & 0xC0 == 0x80);
if cut {
return have;
}
at += 1;
}
0
}
Encoding::Utf16 { le } | Encoding::Ucs2 { le } => {
let odd = src.len() % 2;
let units = src.len() - odd;
if units >= 2 {
let last = &src[units - 2..units];
let unit = if *le {
u32::from(last[0]) | u32::from(last[1]) << 8
} else {
u32::from(last[0]) << 8 | u32::from(last[1])
};
if (0xD800..0xDC00).contains(&unit) {
return odd + 2;
}
}
odd
}
Encoding::Utf32 { .. } => src.len() % 4,
}
}
pub struct Table {
to_unicode: Vec<Option<Box<[u16; 256]>>>,
from_unicode: Vec<Option<Box<[u16; 256]>>>,
prefix: [bool; 256],
fallback: u16,
}
fn table(name: &str) -> Option<&'static Table> {
static LOADED: OnceLock<Mutex<HashMap<&'static str, &'static Table>>> = OnceLock::new();
let cache = LOADED.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().expect("encoding table cache");
let (name, text) = crate::encoding_tables::TABLES
.iter()
.find(|(candidate, _)| *candidate == name)?;
if let Some(table) = cache.get(name) {
return Some(table);
}
let table: &'static Table = Box::leak(Box::new(load_table(text)?));
cache.insert(name, table);
Some(table)
}
fn load_table(text: &str) -> Option<Table> {
let mut lines = text.lines();
lines.next()?; let kind = lines.next()?.trim();
let header = lines.next()?;
let mut fields = header.split_whitespace();
let fallback = u16::from_str_radix(fields.next()?, 16).ok()?;
let symbol: u32 = fields.next()?.parse().ok()?;
let pages: usize = fields.next()?.parse().ok()?;
let pages = pages.min(256);
let mut to_unicode: Vec<Option<Box<[u16; 256]>>> = (0..256).map(|_| None).collect();
for _ in 0..pages {
let hi = hex4(lines.next()?.as_bytes(), 0) >> 8;
let mut page = Box::new([0u16; 256]);
for row in 0..16 {
let bytes = lines.next()?.as_bytes();
for column in 0..16 {
page[row * 16 + column] = hex4(bytes, column * 4);
}
}
to_unicode[hi as usize] = Some(page);
}
let mut prefix = [false; 256];
if kind == "D" {
prefix = [true; 256];
} else {
for (hi, page) in to_unicode.iter().enumerate().skip(1) {
prefix[hi] = page.is_some();
}
}
let mut from_unicode: Vec<Option<Box<[u16; 256]>>> = (0..256).map(|_| None).collect();
if symbol != 0 {
from_unicode[0] = Some(Box::new([0u16; 256]));
}
for (hi, slot) in to_unicode.iter().enumerate() {
let Some(page) = slot else { continue };
for (lo, entry) in page.iter().enumerate() {
let ch = *entry as usize;
if ch == 0 {
continue;
}
from_unicode[ch >> 8]
.get_or_insert_with(|| Box::new([0u16; 256]))
.as_mut()[ch & 0xFF] = ((hi << 8) | lo) as u16;
}
}
if kind == "M" {
if let Some(page) = &mut from_unicode[0] {
if page[usize::from(b'\\')] == 0 {
page[usize::from(b'\\')] = u16::from(b'\\');
}
}
}
if symbol != 0 {
let zero = to_unicode[0]
.clone()
.unwrap_or_else(|| Box::new([0u16; 256]));
let page = from_unicode[0].get_or_insert_with(|| Box::new([0u16; 256]));
for lo in 0..256 {
if zero[lo] != 0 {
page[lo] = lo as u16;
}
}
}
let mut rest = lines.skip_while(|line| line.is_empty());
if rest.next().is_some_and(|line| line.starts_with('R')) {
for line in rest {
let bytes = line.as_bytes();
if bytes.len() < 5 {
continue;
}
let to = hex4(bytes, 0);
if to == 0 {
continue;
}
let mut p = 5;
while p + 4 <= bytes.len() {
let from = hex4(bytes, p) as usize;
p += 5;
if from == 0 {
continue;
}
from_unicode[from >> 8]
.get_or_insert_with(|| Box::new([0u16; 256]))
.as_mut()[from & 0xFF] = to;
}
}
}
Some(Table {
to_unicode,
from_unicode,
prefix,
fallback,
})
}
fn hex4(bytes: &[u8], at: usize) -> u16 {
let mut value = 0u16;
for offset in 0..4 {
let digit = match bytes.get(at + offset) {
Some(b'0'..=b'9') => bytes[at + offset] - b'0',
Some(b'a'..=b'f') => bytes[at + offset] - b'a' + 10,
Some(b'A'..=b'F') => bytes[at + offset] - b'A' + 10,
_ => 0,
};
value = (value << 4) | u16::from(digit);
}
value
}
fn page(pages: &[Option<Box<[u16; 256]>>], hi: usize) -> &[u16; 256] {
const EMPTY: [u16; 256] = [0; 256];
pages[hi].as_deref().unwrap_or(&EMPTY)
}
fn to_utf(
encoding: &Encoding,
src: &[u8],
profile: Profile,
) -> Result<(String, Option<usize>), String> {
match encoding {
Encoding::Table(table) => Ok(table_to_utf(table, src, profile)),
Encoding::Utf8 => utf8_to_utf(src, profile, true),
Encoding::Cesu8 => utf8_to_utf(src, profile, false),
Encoding::Utf16 { le } | Encoding::Ucs2 { le } => utf16_to_utf(src, *le, profile),
Encoding::Utf32 { le } => utf32_to_utf(src, *le, profile),
}
}
fn push_char(out: &mut String, cp: u32) -> Result<(), String> {
match char::from_u32(cp) {
Some(ch) => {
out.push(ch);
Ok(())
}
None => Err(format!(
"encoding convertfrom: the tcl8 profile decodes this input to the lone surrogate \
U+{cp:04X}, which a string in this frontend cannot hold"
)),
}
}
fn table_to_utf(table: &Table, src: &[u8], profile: Profile) -> (String, Option<usize>) {
let mut out = String::new();
let mut at = 0;
while at < src.len() {
let byte = src[at] as usize;
let mut consumed = 1;
let mut ch = if table.prefix[byte] {
if at + 1 >= src.len() {
match profile {
Profile::Strict => return (out, Some(at)),
Profile::Replace => REPLACE_CHAR,
Profile::Tcl8 => byte as u32,
}
} else {
consumed = 2;
u32::from(page(&table.to_unicode, byte)[src[at + 1] as usize])
}
} else {
u32::from(page(&table.to_unicode, 0)[byte])
};
if ch == 0 && byte != 0 {
if profile == Profile::Strict {
return (out, Some(at + consumed - 1));
}
consumed = 1;
ch = match profile {
Profile::Replace => REPLACE_CHAR,
_ => lone_byte(byte as u8),
};
}
out.push(char::from_u32(ch).unwrap_or(char::REPLACEMENT_CHARACTER));
at += consumed;
}
(out, None)
}
fn lone_byte(byte: u8) -> u32 {
const CP1252_HIGH: [u16; 32] = [
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160,
0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022,
0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
];
if (0x80..0xA0).contains(&byte) {
u32::from(CP1252_HIGH[usize::from(byte) - 0x80])
} else {
u32::from(byte)
}
}
fn utf8_to_utf(src: &[u8], profile: Profile, utf: bool) -> Result<(String, Option<usize>), String> {
let mut out = String::new();
let mut at = 0;
let mut pending: Option<u32> = None;
while at < src.len() {
if src[at] == 0xC0 && at + 1 < src.len() && src[at + 1] == 0x80 {
if let Some(high) = pending.take() {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
}
match profile {
Profile::Tcl8 => {
out.push('\0');
at += 2;
}
Profile::Replace => {
push_char(&mut out, REPLACE_CHAR)?;
at += 2;
}
Profile::Strict => return Ok((out, Some(at))),
}
continue;
}
let (cp, len) = match decode_utf8(&src[at..]) {
Some(pair) => pair,
None => {
if let Some(high) = pending.take() {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
continue;
}
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => {
push_char(&mut out, REPLACE_CHAR)?;
at += 1;
}
Profile::Tcl8 => {
push_char(&mut out, lone_byte(src[at]))?;
at += 1;
}
}
continue;
}
};
if is_surrogate(cp) {
if utf {
if let Some(high) = pending.take() {
push_char(&mut out, high)?;
}
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => {
push_char(&mut out, REPLACE_CHAR)?;
at += len;
}
Profile::Tcl8 => {
push_char(&mut out, cp)?;
at += len;
}
}
continue;
}
if (0xDC00..0xE000).contains(&cp) {
match pending.take() {
Some(high) => {
push_char(&mut out, 0x10000 + ((high - 0xD800) << 10) + (cp - 0xDC00))?;
at += len;
}
None => match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => {
push_char(&mut out, REPLACE_CHAR)?;
at += len;
}
Profile::Tcl8 => {
push_char(&mut out, cp)?;
at += len;
}
},
}
continue;
}
if let Some(high) = pending.take() {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
}
pending = Some(cp);
at += len;
continue;
}
if let Some(high) = pending.take() {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
}
if !utf && cp > 0xFFFF {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => {
push_char(&mut out, REPLACE_CHAR)?;
at += len;
}
Profile::Tcl8 => {
push_char(&mut out, cp)?;
at += len;
}
}
continue;
}
push_char(&mut out, cp)?;
at += len;
}
if let Some(high) = pending {
match profile {
Profile::Strict => return Ok((out, Some(src.len()))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
}
Ok((out, None))
}
fn decode_utf8(src: &[u8]) -> Option<(u32, usize)> {
let lead = src[0];
let (len, mut cp) = match lead {
0x00..=0x7F => return Some((u32::from(lead), 1)),
0xC2..=0xDF => (2, u32::from(lead & 0x1F)),
0xC0..=0xC1 => return None, 0xE0..=0xEF => (3, u32::from(lead & 0x0F)),
0xF0..=0xF4 => (4, u32::from(lead & 0x07)),
_ => return None,
};
if src.len() < len {
return None;
}
for byte in &src[1..len] {
if byte & 0xC0 != 0x80 {
return None;
}
cp = (cp << 6) | u32::from(byte & 0x3F);
}
if (len == 3 && cp < 0x800) || (len == 4 && cp < 0x10000) || cp > 0x10FFFF {
return None;
}
Some((cp, len))
}
fn is_surrogate(cp: u32) -> bool {
(0xD800..0xE000).contains(&cp)
}
fn utf16_to_utf(src: &[u8], le: bool, profile: Profile) -> Result<(String, Option<usize>), String> {
let mut out = String::new();
let whole = src.len() - src.len() % 2;
let mut at = 0;
let mut pending: Option<u32> = None;
while at < whole {
let unit = if le {
u32::from(src[at]) | u32::from(src[at + 1]) << 8
} else {
u32::from(src[at]) << 8 | u32::from(src[at + 1])
};
if let Some(high) = pending.take() {
if (0xDC00..0xE000).contains(&unit) {
push_char(
&mut out,
0x10000 + ((high - 0xD800) << 10) + (unit - 0xDC00),
)?;
at += 2;
continue;
}
match profile {
Profile::Strict => return Ok((out, Some(at - 2))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
continue;
}
if (0xD800..0xDC00).contains(&unit) {
pending = Some(unit);
at += 2;
continue;
}
if is_surrogate(unit) {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, unit)?,
}
at += 2;
continue;
}
push_char(&mut out, unit)?;
at += 2;
}
if let Some(high) = pending {
match profile {
Profile::Strict => return Ok((out, Some(whole - 2))),
Profile::Replace => push_char(&mut out, REPLACE_CHAR)?,
Profile::Tcl8 => push_char(&mut out, high)?,
}
}
if whole != src.len() {
match profile {
Profile::Strict => return Ok((out, Some(whole))),
_ => push_char(&mut out, REPLACE_CHAR)?,
}
}
Ok((out, None))
}
fn utf32_to_utf(src: &[u8], le: bool, profile: Profile) -> Result<(String, Option<usize>), String> {
let mut out = String::new();
let whole = src.len() - src.len() % 4;
let mut at = 0;
while at < whole {
let quad = &src[at..at + 4];
let cp = if le {
u32::from(quad[3]) << 24
| u32::from(quad[2]) << 16
| u32::from(quad[1]) << 8
| u32::from(quad[0])
} else {
u32::from(quad[0]) << 24
| u32::from(quad[1]) << 16
| u32::from(quad[2]) << 8
| u32::from(quad[3])
};
let cp = if cp > 0x10FFFF {
if profile == Profile::Strict {
return Ok((out, Some(at)));
}
REPLACE_CHAR
} else if is_surrogate(cp) {
match profile {
Profile::Strict => return Ok((out, Some(at))),
Profile::Replace => REPLACE_CHAR,
Profile::Tcl8 => cp,
}
} else {
cp
};
push_char(&mut out, cp)?;
at += 4;
}
if whole != src.len() {
match profile {
Profile::Strict => return Ok((out, Some(whole))),
_ => push_char(&mut out, REPLACE_CHAR)?,
}
}
Ok((out, None))
}
fn from_utf(encoding: &Encoding, src: &str, profile: Profile) -> (Vec<u8>, Option<usize>) {
match encoding {
Encoding::Table(table) => table_from_utf(table, src, profile),
Encoding::Utf8 => (src.as_bytes().to_vec(), None),
Encoding::Cesu8 => cesu8_from_utf(src),
Encoding::Utf16 { le } => utf16_from_utf(src, *le),
Encoding::Ucs2 { le } => ucs2_from_utf(src, *le, profile),
Encoding::Utf32 { le } => utf32_from_utf(src, *le),
}
}
fn table_from_utf(table: &Table, src: &str, profile: Profile) -> (Vec<u8>, Option<usize>) {
let mut out = Vec::new();
for (at, ch) in src.char_indices() {
let cp = u32::from(ch);
let mut word = if cp > 0xFFFF {
0
} else {
page(&table.from_unicode, (cp >> 8) as usize)[(cp & 0xFF) as usize]
};
if word == 0 && cp != 0 {
if profile == Profile::Strict {
return (out, Some(at));
}
word = table.fallback;
}
if table.prefix[usize::from(word >> 8)] {
out.push((word >> 8) as u8);
out.push(word as u8);
} else {
out.push(word as u8);
}
}
(out, None)
}
fn cesu8_from_utf(src: &str) -> (Vec<u8>, Option<usize>) {
let mut out = Vec::new();
for ch in src.chars() {
let cp = u32::from(ch);
if cp > 0xFFFF {
let cp = cp - 0x10000;
push_three(&mut out, 0xD800 + (cp >> 10));
push_three(&mut out, 0xDC00 + (cp & 0x3FF));
} else if cp > 0x7FF {
push_three(&mut out, cp);
} else {
let mut buf = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
}
}
(out, None)
}
fn push_three(out: &mut Vec<u8>, cp: u32) {
out.push(0xE0 | (cp >> 12) as u8);
out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
out.push(0x80 | (cp & 0x3F) as u8);
}
fn utf16_from_utf(src: &str, le: bool) -> (Vec<u8>, Option<usize>) {
let mut out = Vec::new();
for ch in src.chars() {
let cp = u32::from(ch);
if cp > 0xFFFF {
let cp = cp - 0x10000;
push_unit(&mut out, 0xD800 + (cp >> 10), le);
push_unit(&mut out, 0xDC00 + (cp & 0x3FF), le);
} else {
push_unit(&mut out, cp, le);
}
}
(out, None)
}
fn ucs2_from_utf(src: &str, le: bool, profile: Profile) -> (Vec<u8>, Option<usize>) {
let mut out = Vec::new();
for (at, ch) in src.char_indices() {
let cp = u32::from(ch);
let cp = if cp > 0xFFFF {
if profile == Profile::Strict {
return (out, Some(at));
}
REPLACE_CHAR
} else {
cp
};
push_unit(&mut out, cp, le);
}
(out, None)
}
fn push_unit(out: &mut Vec<u8>, unit: u32, le: bool) {
if le {
out.push(unit as u8);
out.push((unit >> 8) as u8);
} else {
out.push((unit >> 8) as u8);
out.push(unit as u8);
}
}
fn utf32_from_utf(src: &str, le: bool) -> (Vec<u8>, Option<usize>) {
let mut out = Vec::new();
for ch in src.chars() {
let cp = u32::from(ch);
let quad = [
(cp >> 24) as u8,
(cp >> 16) as u8,
(cp >> 8) as u8,
cp as u8,
];
if le {
out.extend(quad.iter().rev());
} else {
out.extend_from_slice(&quad);
}
}
(out, None)
}