use std::sync::Arc;
use fusevm::{Op, Value, VM};
use num_bigint::{BigInt, BigUint};
use crate::assoc::{target_of, Target};
use crate::compiler::{CompileError, Compiler};
use crate::list;
use crate::parser::Word;
use crate::runtime::{format_double, place_at, to_tcl_string, var_cell};
pub mod ext {
pub use crate::compiler::ext::BINARY_BASE as BASE;
pub const FORMAT: u16 = BASE;
pub const SCAN: u16 = BASE + 1;
pub const ENCODE: u16 = BASE + 2;
pub const DECODE: u16 = BASE + 3;
}
pub const COMMANDS: &[&str] = &["binary"];
pub const SUBCOMMANDS: &[&str] = &["decode", "encode", "format", "scan"];
pub const CODECS: &[&str] = &["base64", "hex", "uuencode"];
pub(crate) fn compile(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let Some(first) = args.first() else {
return c.error("wrong # args: should be \"binary 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 {
"format" => compile_format(c, rest),
"scan" => compile_scan(c, rest),
"encode" => compile_codec(c, rest, ext::ENCODE, "encode"),
_ => compile_codec(c, rest, ext::DECODE, "decode"),
}
}
fn compile_format(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return c.error("wrong # args: should be \"binary format formatString ?arg ...?\"");
}
for w in args {
c.word(w)?;
}
c.emit(Op::LoadInt(args.len() as i64 - 1), 1);
c.emit(Op::Extended(ext::FORMAT, 0), -(args.len() as i32));
Ok(())
}
fn compile_scan(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
if args.len() < 2 {
return c.error("wrong # args: should be \"binary scan value formatString ?varName ...?\"");
}
let mut names = Vec::with_capacity(args.len() - 2);
for w in &args[2..] {
match target_of(w) {
Some(Target::Scalar(name)) => names.push(name),
_ => return c.error("\"binary scan\" into an array element is not supported yet"),
}
}
c.word(&args[0])?;
c.word(&args[1])?;
for name in &names {
c.push_str(name);
let place = c.var_place(name);
c.push_value(Value::Int(i64::from(place.in_frame())));
c.emit(Op::LoadInt(place.frame_operand()), 1);
}
c.emit(Op::LoadInt(names.len() as i64), 1);
c.emit(Op::Extended(ext::SCAN, 0), -(3 * names.len() as i32 + 2));
Ok(())
}
fn compile_codec(
c: &mut Compiler,
args: &[Word],
id: u16,
verb: &str,
) -> Result<(), CompileError> {
let Some(first) = args.first() else {
return c.error(format!(
"wrong # args: should be \"binary {verb} subcommand ?arg ...?\""
));
};
let given = c.literal_of(first, "subcommand")?.to_string();
let Some(codec) = CODECS.iter().position(|name| *name == given) else {
return c.error(format!(
"unknown subcommand \"{given}\": must be {}",
listing(CODECS)
));
};
let rest = &args[1..];
if rest.is_empty() {
return c.error(format!("wrong # args: should be \"{}\"", usage(verb, codec)));
}
for w in rest {
c.word(w)?;
}
c.emit(Op::LoadInt(rest.len() as i64), 1);
c.emit(Op::Extended(id, codec as u8), -(rest.len() as i32));
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
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Count {
One,
All,
Num(usize),
}
struct Spec {
first: char,
cmd: char,
unsigned: bool,
count: Count,
}
fn next_spec(f: &[char], i: &mut usize) -> Option<Spec> {
let start = *i;
while f.get(*i) == Some(&' ') {
*i += 1;
}
let cmd = *f.get(*i)?;
*i += 1;
let unsigned = f.get(*i) == Some(&'u');
if unsigned {
*i += 1;
}
let count = if f.get(*i) == Some(&'*') {
*i += 1;
Count::All
} else if f.get(*i).is_some_and(char::is_ascii_digit) {
let from = *i;
while f.get(*i).is_some_and(char::is_ascii_digit) {
*i += 1;
}
let text: String = f[from..*i].iter().collect();
Count::Num(text.parse().unwrap_or(usize::MAX))
} else {
Count::One
};
Some(Spec {
first: f[start],
cmd,
unsigned,
count,
})
}
fn bad_field(spec: &Spec) -> String {
format!("bad field specifier \"{}\"", spec.first)
}
fn item_size(cmd: char) -> Option<usize> {
Some(match cmd {
'c' => 1,
's' | 'S' | 't' => 2,
'i' | 'I' | 'n' | 'f' | 'r' | 'R' => 4,
'w' | 'W' | 'm' | 'd' | 'q' | 'Q' => 8,
_ => return None,
})
}
fn big_endian(cmd: char) -> bool {
match cmd {
'S' | 'I' | 'W' | 'R' | 'Q' => true,
't' | 'n' | 'm' | 'f' | 'd' => cfg!(target_endian = "big"),
_ => false,
}
}
pub(crate) 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)
}
pub(crate) fn from_bytes(bytes: &[u8]) -> String {
bytes.iter().map(|b| char::from(*b)).collect()
}
const MAX_RESULT_BYTES: usize = 2 * 1024 * 1024 * 1024;
struct Planned {
cmd: char,
count: usize,
arg: usize,
scalar: bool,
}
fn format(fmt: &str, args: &[String]) -> Result<Vec<u8>, String> {
let f: Vec<char> = fmt.chars().collect();
let (plan, length) = plan_format(&f, args)?;
let mut out = vec![0u8; length];
let mut at = 0usize;
for step in plan {
let count = step.count;
match step.cmd {
'a' | 'A' => {
let pad = if step.cmd == 'A' { b' ' } else { 0 };
let bytes = low_bytes(&args[step.arg]);
for slot in 0..count {
out[at + slot] = bytes.get(slot).copied().unwrap_or(pad);
}
at += count;
}
'b' | 'B' => {
let bits: Vec<char> = args[step.arg].chars().collect();
let width = count.div_ceil(8);
for byte in 0..width {
let mut value = 0u8;
for bit in 0..8 {
let index = byte * 8 + bit;
if index >= count {
break;
}
let one = match bits.get(index) {
Some('1') => true,
Some('0') => false,
Some(_) => {
return Err(format!(
"expected binary string but got \"{}\" instead",
cut(&args[step.arg])
))
}
None => false,
};
if one {
value |= if step.cmd == 'b' { 1 << bit } else { 0x80 >> bit };
}
}
out[at + byte] = value;
}
at += width;
}
'h' | 'H' => {
let text: Vec<char> = args[step.arg].chars().collect();
let width = count.div_ceil(2);
for byte in 0..width {
let mut value = 0u8;
for half in 0..2 {
let index = byte * 2 + half;
if index >= count {
break;
}
let digit = match text.get(index) {
Some(ch) => ch.to_digit(16).ok_or_else(|| {
format!(
"expected hexadecimal string but got \"{}\" instead",
cut(&args[step.arg])
)
})? as u8,
None => 0,
};
let high = (step.cmd == 'H') == (half == 0);
value |= if high { digit << 4 } else { digit };
}
out[at + byte] = value;
}
at += width;
}
'x' => {
out[at..at + count].fill(0);
at += count;
}
'X' => at = at.saturating_sub(count),
'@' => at = count,
cmd => {
let size = item_size(cmd).expect("plan_format rejected every other type");
let values = numeric_items(&args[step.arg], count, step.scalar)?;
for value in values {
write_number(&mut out[at..at + size], cmd, &value)?;
at += size;
}
}
}
}
Ok(out)
}
fn plan_format(f: &[char], args: &[String]) -> Result<(Vec<Planned>, usize), String> {
let mut plan = Vec::new();
let mut at = 0usize;
let mut length = 0usize;
let mut arg = 0usize;
let mut i = 0usize;
while let Some(spec) = next_spec(f, &mut i) {
let consumes = !matches!(spec.cmd, 'x' | 'X' | '@');
if consumes && arg >= args.len() {
if item_size(spec.cmd).is_none() && !matches!(spec.cmd, 'a' | 'A' | 'b' | 'B' | 'h' | 'H')
{
return Err(bad_field(&spec));
}
return Err("not enough arguments for all format specifiers".to_string());
}
let count = match spec.cmd {
'a' | 'A' => match spec.count {
Count::All => args[arg].chars().count(),
Count::One => 1,
Count::Num(n) => n,
},
'b' | 'B' | 'h' | 'H' => match spec.count {
Count::All => args[arg].chars().count(),
Count::One => 1,
Count::Num(n) => n,
},
'x' => match spec.count {
Count::All => return Err("cannot use \"*\" in format string with \"x\"".to_string()),
Count::One => 1,
Count::Num(n) => n,
},
'X' => match spec.count {
Count::All => at,
Count::One => 1,
Count::Num(n) => n,
},
'@' => match spec.count {
Count::One => {
return Err("missing count for \"@\" field specifier".to_string())
}
Count::All => length,
Count::Num(n) => n,
},
cmd if item_size(cmd).is_some() => match spec.count {
Count::One => 1,
Count::All => list::length(&args[arg])?,
Count::Num(n) => {
if list::length(&args[arg])? < n {
return Err(
"number of elements in list does not match count".to_string()
);
}
n
}
},
_ => return Err(bad_field(&spec)),
};
let step = Planned {
cmd: spec.cmd,
count,
arg,
scalar: spec.count == Count::One,
};
if consumes {
arg += 1;
}
match spec.cmd {
'a' | 'A' => at += count,
'b' | 'B' => at += count.div_ceil(8),
'h' | 'H' => at += count.div_ceil(2),
'x' => at += count,
'X' => at = at.saturating_sub(count),
'@' => at = count,
cmd => at += count * item_size(cmd).expect("checked above"),
}
if at > length {
length = at;
}
if length > MAX_RESULT_BYTES {
return Err("max size for a Tcl value exceeded".to_string());
}
plan.push(step);
}
Ok((plan, length))
}
fn numeric_items(arg: &str, count: usize, scalar: bool) -> Result<Vec<String>, String> {
if scalar {
return Ok(vec![arg.to_string()]);
}
let mut items = list::split(arg)?;
items.truncate(count);
Ok(items)
}
fn write_number(slot: &mut [u8], cmd: char, text: &str) -> Result<(), String> {
let bytes: Vec<u8> = match cmd {
'f' | 'r' | 'R' => (double(text)? as f32).to_le_bytes().to_vec(),
'd' | 'q' | 'Q' => double(text)?.to_le_bytes().to_vec(),
_ => {
let value = crate::cmd_string::parse_big(text.trim_matches(is_space))
.ok_or_else(|| {
format!(
"expected integer but got {}",
crate::runtime::named(text, 50)
)
})?;
truncated(&value, slot.len() * 8)
}
};
if big_endian(cmd) {
for (i, b) in bytes.iter().rev().enumerate() {
slot[i] = *b;
}
} else {
slot.copy_from_slice(&bytes);
}
Ok(())
}
fn truncated(value: &BigInt, bits: usize) -> Vec<u8> {
let mask = BigInt::from((BigUint::from(1u8) << bits) - BigUint::from(1u8));
let pattern = (value & &mask).magnitude().to_bytes_le();
let mut out = vec![0u8; bits / 8];
for (i, b) in pattern.iter().take(bits / 8).enumerate() {
out[i] = *b;
}
out
}
fn double(text: &str) -> Result<f64, String> {
crate::cmd_string::parse_double(text).ok_or_else(|| {
format!(
"expected floating-point number but got {}",
crate::runtime::named(text, 50)
)
})
}
fn low_bytes(text: &str) -> Vec<u8> {
text.chars().map(|c| (u32::from(c) & 0xff) as u8).collect()
}
fn is_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\u{b}' | '\u{c}' | '\r')
}
fn cut(text: &str) -> &str {
let mut end = text.len().min(50);
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
struct Scanned {
values: Vec<Option<String>>,
}
fn scan(data: &[u8], fmt: &str, vars: usize) -> Result<Scanned, String> {
let f: Vec<char> = fmt.chars().collect();
let mut wanted = 0usize;
let mut i = 0usize;
while let Some(spec) = next_spec(&f, &mut i) {
match spec.cmd {
'a' | 'A' | 'C' | 'b' | 'B' | 'h' | 'H' => wanted += 1,
'x' | 'X' | '@' => {
if spec.cmd == '@' && spec.count == Count::One {
return Err("missing count for \"@\" field specifier".to_string());
}
}
cmd if item_size(cmd).is_some() => wanted += 1,
_ => return Err(bad_field(&spec)),
}
}
if wanted > vars {
return Err("not enough arguments for all format specifiers".to_string());
}
let mut values = vec![None; wanted];
let mut slot = 0usize;
let mut at = 0usize;
let mut i = 0usize;
while let Some(spec) = next_spec(&f, &mut i) {
let left = data.len() - at.min(data.len());
match spec.cmd {
'a' | 'A' | 'C' => {
let count = match spec.count {
Count::All => left,
Count::One => 1,
Count::Num(n) => n,
};
if count > left {
break;
}
let mut taken = &data[at..at + count];
if spec.cmd == 'A' {
while taken.last().is_some_and(|b| *b == b' ' || *b == 0) {
taken = &taken[..taken.len() - 1];
}
} else if spec.cmd == 'C' {
if let Some(end) = taken.iter().position(|b| *b == 0) {
taken = &taken[..end];
}
while taken.last().is_some_and(|b| *b == b' ') {
taken = &taken[..taken.len() - 1];
}
}
values[slot] = Some(from_bytes(taken));
slot += 1;
at += count;
}
'b' | 'B' => {
let count = match spec.count {
Count::All => left * 8,
Count::One => 1,
Count::Num(n) => n,
};
if count.div_ceil(8) > left {
break;
}
let mut text = String::with_capacity(count);
for bit in 0..count {
let byte = data[at + bit / 8];
let taken = if spec.cmd == 'b' {
byte >> (bit % 8) & 1
} else {
byte >> (7 - bit % 8) & 1
};
text.push(if taken == 1 { '1' } else { '0' });
}
values[slot] = Some(text);
slot += 1;
at += count.div_ceil(8);
}
'h' | 'H' => {
let count = match spec.count {
Count::All => left * 2,
Count::One => 1,
Count::Num(n) => n,
};
if count.div_ceil(2) > left {
break;
}
let mut text = String::with_capacity(count);
for half in 0..count {
let byte = data[at + half / 2];
let high = (spec.cmd == 'H') == (half % 2 == 0);
let digit = if high { byte >> 4 } else { byte & 0xf };
text.push(char::from_digit(u32::from(digit), 16).expect("a nibble"));
}
values[slot] = Some(text);
slot += 1;
at += count.div_ceil(2);
}
'x' => {
let count = match spec.count {
Count::All => left,
Count::One => 1,
Count::Num(n) => n,
};
at = (at + count).min(data.len());
}
'X' => {
let count = match spec.count {
Count::All => at,
Count::One => 1,
Count::Num(n) => n,
};
at = at.saturating_sub(count);
}
'@' => {
let count = match spec.count {
Count::All => data.len(),
Count::One => unreachable!("refused by the validation pass"),
Count::Num(n) => n,
};
at = count.min(data.len());
}
cmd => {
let size = item_size(cmd).expect("the validation pass rejected every other type");
let count = match spec.count {
Count::All => left / size,
Count::One => 1,
Count::Num(n) => n,
};
if count * size > left {
break;
}
let mut items = Vec::with_capacity(count);
for item in 0..count {
let from = at + item * size;
items.push(read_number(&data[from..from + size], cmd, spec.unsigned));
}
values[slot] = Some(list::join(&items));
slot += 1;
at += count * size;
}
}
}
Ok(Scanned { values })
}
fn read_number(slot: &[u8], cmd: char, unsigned: bool) -> String {
let mut bytes = slot.to_vec();
if big_endian(cmd) {
bytes.reverse();
}
match cmd {
'f' | 'r' | 'R' => {
let raw = u32::from_le_bytes(bytes.try_into().expect("four bytes"));
format_double(f64::from(f32::from_bits(raw)))
}
'd' | 'q' | 'Q' => {
let raw = u64::from_le_bytes(bytes.try_into().expect("eight bytes"));
format_double(f64::from_bits(raw))
}
_ => {
let mut value: u128 = 0;
for (i, b) in bytes.iter().enumerate() {
value |= u128::from(*b) << (8 * i);
}
let bits = bytes.len() * 8;
if !unsigned && value >> (bits - 1) & 1 == 1 {
(value as i128 - (1i128 << bits)).to_string()
} else {
value.to_string()
}
}
}
}
const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn encode(codec: usize, data: &[u8], opts: &Options) -> Result<String, String> {
match CODECS[codec] {
"hex" => Ok(data.iter().map(|b| format!("{b:02x}")).collect()),
"base64" => Ok(wrap(&base64_digits(data), opts)),
_ => Ok(uuencode(data, opts)),
}
}
fn base64_digits(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for group in data.chunks(3) {
let mut bits = 0u32;
for (i, b) in group.iter().enumerate() {
bits |= u32::from(*b) << (16 - 8 * i);
}
for i in 0..4 {
if i <= group.len() {
out.push(char::from(BASE64[(bits >> (18 - 6 * i) & 0x3f) as usize]));
} else {
out.push('=');
}
}
}
out
}
fn wrap(text: &str, opts: &Options) -> String {
let Some(max) = opts.maxlen.filter(|n| *n > 0) else {
return text.to_string();
};
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len());
for (i, line) in chars.chunks(max).enumerate() {
if i > 0 {
out.push_str(&opts.wrapchar);
}
out.extend(line);
}
out
}
fn uuencode(data: &[u8], opts: &Options) -> String {
let max = opts.maxlen.unwrap_or(61).clamp(5, 85);
let per_line = ((max - 1) / 4) * 3;
let mut out = String::new();
for line in data.chunks(per_line.max(1)) {
out.push(uu_digit(line.len() as u8));
for group in line.chunks(3) {
let mut bits = 0u32;
for (i, b) in group.iter().enumerate() {
bits |= u32::from(*b) << (16 - 8 * i);
}
for i in 0..=group.len() {
out.push(uu_digit((bits >> (18 - 6 * i) & 0x3f) as u8));
}
}
out.push_str(&opts.wrapchar);
}
out
}
fn uu_digit(value: u8) -> char {
if value == 0 {
'`'
} else {
char::from(value + 0x20)
}
}
fn decode(codec: usize, text: &str, strict: bool) -> Result<Vec<u8>, String> {
match CODECS[codec] {
"hex" => decode_hex(text, strict),
"base64" => decode_base64(text, strict),
_ => decode_uuencode(text, strict),
}
}
fn decode_hex(text: &str, strict: bool) -> Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(text.len() / 2);
let mut pending: Option<u8> = None;
for (at, ch) in text.chars().enumerate() {
if !strict && ch.is_whitespace() {
continue;
}
let Some(digit) = ch.to_digit(16) else {
return Err(format!(
"invalid hexadecimal digit \"{ch}\" (U+{:06X}) at position {at}",
u32::from(ch)
));
};
match pending.take() {
Some(high) => out.push(high << 4 | digit as u8),
None => pending = Some(digit as u8),
}
}
Ok(out)
}
fn decode_base64(text: &str, strict: bool) -> Result<Vec<u8>, String> {
let chars: Vec<char> = text.chars().collect();
let mut out = Vec::with_capacity(chars.len() / 4 * 3);
let mut bits = 0u32;
let mut have = 0u32;
let mut taken = 0usize;
for (at, ch) in chars.iter().enumerate() {
let ch = *ch;
if ch == '=' {
if strict {
if taken % 4 < 2 {
return Err(bad_base64(ch, at));
}
if at + 1 != chars.len() && at + 2 != chars.len() {
return Err(bad_base64('=', at + 1));
}
}
break;
}
let value = if ch.is_ascii() {
BASE64.iter().position(|b| char::from(*b) == ch)
} else {
None
};
let Some(value) = value else {
if !strict {
continue;
}
return Err(bad_base64(ch, at));
};
taken += 1;
bits = bits << 6 | value as u32;
have += 6;
if have >= 8 {
have -= 8;
out.push((bits >> have & 0xff) as u8);
}
}
if strict && taken % 4 == 1 {
let at = chars.len() - 1;
return Err(bad_base64(chars[at], at));
}
Ok(out)
}
fn bad_base64(ch: char, at: usize) -> String {
format!(
"invalid base64 character \"{ch}\" (U+{:06X}) at position {at}",
u32::from(ch)
)
}
fn decode_uuencode(text: &str, strict: bool) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
let mut rest = text;
while !rest.is_empty() {
let (line, terminated) = match rest.find('\n') {
Some(end) => {
let line = &rest[..end];
rest = &rest[end + 1..];
(line, true)
}
None => {
let line = rest;
rest = "";
(line, false)
}
};
let chars: Vec<char> = line.chars().collect();
let Some(first) = chars.first() else {
if strict {
uu_value('\n', true, 0)?;
}
continue;
};
let want = usize::from(uu_value(*first, true, 0)?);
if want == 0 {
break;
}
if strict {
for (at, ch) in chars.iter().enumerate().skip(1) {
uu_value(*ch, true, at)?;
}
}
let needed = if terminated {
want.div_ceil(3) * 4
} else {
want + want.div_ceil(3)
};
if strict && chars.len() - 1 < needed {
return Err("short uuencode data".to_string());
}
let mut wrote = 0usize;
for (group, chunk) in chars[1..].chunks(4).enumerate() {
let mut value = 0u32;
for (i, ch) in chunk.iter().enumerate() {
value |= u32::from(uu_value(*ch, strict, group * 4 + i + 1)?) << (18 - 6 * i);
}
for i in 0..3 {
if wrote < want {
out.push((value >> (16 - 8 * i) & 0xff) as u8);
wrote += 1;
}
}
}
}
Ok(out)
}
fn uu_value(ch: char, strict: bool, at: usize) -> Result<u8, String> {
match ch {
'`' | ' ' => Ok(0),
c if ('!'..='_').contains(&c) => Ok(c as u8 - 0x20),
c if strict => Err(format!(
"invalid uuencode character \"{c}\" (U+{:06X}) at position {at}",
u32::from(c)
)),
c if c.is_whitespace() => Ok(0),
c if c.is_ascii() => Ok((c as u8).wrapping_sub(0x20) & 0x3f),
c => Err(format!(
"invalid uuencode character \"{c}\" (U+{:06X}) at position {at}",
u32::from(c)
)),
}
}
struct Options {
maxlen: Option<usize>,
wrapchar: String,
strict: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
maxlen: None,
wrapchar: "\n".to_string(),
strict: false,
}
}
}
fn usage(verb: &str, codec: usize) -> String {
match (verb, CODECS[codec]) {
("encode", "hex") => "binary encode hex data".to_string(),
("encode", name) => {
format!("binary encode {name} ?-maxlen len? ?-wrapchar char? data")
}
(_, name) => format!("binary decode {name} ?options? data"),
}
}
fn options(words: &[String], encoding: bool, codec: usize) -> Result<(Options, &String), String> {
let verb = if encoding { "encode" } else { "decode" };
let wrong = || format!("wrong # args: should be \"{}\"", usage(verb, codec));
let takes_pairs = encoding && CODECS[codec] != "hex";
let shape = if takes_pairs {
words.len() % 2 == 0
} else if encoding {
words.len() != 1
} else {
words.len() > 2
};
if shape {
return Err(wrong());
}
let Some((data, opts)) = words.split_last() else {
return Err(wrong());
};
let mut out = Options::default();
let mut i = 0;
while i < opts.len() {
if !encoding {
if opts[i] != "-strict" {
return Err(format!(
"bad option \"{}\": must be -strict",
opts[i]
));
}
out.strict = true;
i += 1;
continue;
}
match opts[i].as_str() {
"-maxlen" => out.maxlen = Some(maxlen(&opts[i + 1], codec)?),
"-wrapchar" => {
if CODECS[codec] == "uuencode"
&& !opts[i + 1]
.chars()
.all(|c| matches!(c, '\t' | '\u{b}' | '\u{c}' | '\r' | '\n'))
{
return Err("invalid wrapchar; will defeat decoding".to_string());
}
out.wrapchar.clone_from(&opts[i + 1]);
}
other => {
return Err(format!(
"bad option \"{other}\": must be -maxlen or -wrapchar"
))
}
}
i += 2;
}
Ok((out, data))
}
fn maxlen(text: &str, codec: usize) -> Result<usize, String> {
let value = list::wide(text)?;
let ok = if CODECS[codec] == "uuencode" {
(5..=85).contains(&value)
} else {
value >= 0
};
if !ok {
return Err("line length out of range".to_string());
}
Ok(value as usize)
}
pub(crate) fn is_op(id: u16) -> bool {
(ext::BASE..ext::BASE + crate::compiler::ext::BLOCK).contains(&id)
}
pub(crate) fn extension(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::FORMAT => {
let count = popped_count(vm);
let mut args = Vec::with_capacity(count);
for _ in 0..count {
args.push(to_tcl_string(&vm.pop()));
}
args.reverse();
let fmt = to_tcl_string(&vm.pop());
let bytes = format(&fmt, &args)?;
vm.push(Value::Str(Arc::new(from_bytes(&bytes))));
Ok(())
}
ext::SCAN => {
let count = popped_count(vm);
let mut places = Vec::with_capacity(count);
for _ in 0..count {
let operand = vm.pop();
let in_frame = to_tcl_string(&vm.pop()) == "1";
let _name = vm.pop();
places.push(place_at(&operand, in_frame)?);
}
places.reverse();
let fmt = to_tcl_string(&vm.pop());
let data = as_bytes(&to_tcl_string(&vm.pop()))?;
let scanned = scan(&data, &fmt, count)?;
let mut assigned = 0i64;
for (place, value) in places.into_iter().zip(scanned.values) {
let Some(value) = value else { continue };
assigned += 1;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::new(value));
}
}
vm.push(Value::Int(assigned));
Ok(())
}
ext::ENCODE | ext::DECODE => {
let count = popped_count(vm);
let mut words = Vec::with_capacity(count);
for _ in 0..count {
words.push(to_tcl_string(&vm.pop()));
}
words.reverse();
let encoding = id == ext::ENCODE;
let (opts, data) = options(&words, encoding, usize::from(arg))?;
let answer = if encoding {
encode(usize::from(arg), &as_bytes(data)?, &opts)?
} else {
from_bytes(&decode(usize::from(arg), data, opts.strict)?)
};
vm.push(Value::Str(Arc::new(answer)));
Ok(())
}
other => Err(format!("unknown extension op {other}")),
}
}
fn popped_count(vm: &mut VM) -> usize {
to_tcl_string(&vm.pop())
.parse()
.expect("the count is emitted as an integer")
}