use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::compiler::{ext, CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{place_at, to_tcl_string, var_cell, Output};
pub const TCL_READABLE: i32 = 1 << 1;
pub const TCL_WRITABLE: i32 = 1 << 2;
pub const TCL_EXCEPTION: i32 = 1 << 3;
pub const TCL_STDIN: i32 = 1 << 1;
pub const TCL_STDOUT: i32 = 1 << 2;
pub const TCL_STDERR: i32 = 1 << 3;
pub const DEFAULT_BUFFER_SIZE: i64 = 4096;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Whence {
Start,
Current,
End,
}
pub trait Device {
fn type_name(&self) -> &str;
fn read(&mut self, buf: &mut [u8]) -> Result<usize, String>;
fn write(&mut self, buf: &[u8]) -> Result<usize, String>;
fn seek(&mut self, _offset: i64, _whence: Whence) -> Result<i64, String> {
Err("illegal seek".to_string())
}
fn seekable(&self) -> bool {
false
}
fn close(&mut self) -> Result<(), String>;
fn handle(&self, _direction: i32) -> Option<isize> {
None
}
fn watch(&mut self, _mask: i32) {}
fn set_option(&mut self, _name: &str, _value: &str) -> Option<Result<(), String>> {
None
}
fn get_option(&mut self, _name: &str) -> Option<Result<String, String>> {
None
}
fn driver_table(&self) -> Option<usize> {
None
}
fn instance_data(&self) -> Option<usize> {
None
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Translation {
Auto,
Lf,
Cr,
Crlf,
}
impl Translation {
fn name(self) -> &'static str {
match self {
Translation::Auto => "auto",
Translation::Lf => "lf",
Translation::Cr => "cr",
Translation::Crlf => "crlf",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Buffering {
Full,
Line,
None,
}
impl Buffering {
fn name(self) -> &'static str {
match self {
Buffering::Full => "full",
Buffering::Line => "line",
Buffering::None => "none",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Encoding {
Utf8,
Iso8859_1,
Named(&'static str),
}
impl Encoding {
fn name(self) -> &'static str {
match self {
Encoding::Utf8 => "utf-8",
Encoding::Iso8859_1 => "iso8859-1",
Encoding::Named(name) => name,
}
}
}
struct Channel {
name: String,
device: Box<dyn Device>,
mode: i32,
ref_count: isize,
input_translation: Translation,
output_translation: Translation,
encoding: Encoding,
buffering: Buffering,
buffer_size: i64,
blocking: bool,
raw: Vec<u8>,
pending: String,
device_eof: bool,
saw_cr: bool,
out: Vec<u8>,
handlers: Vec<ChannelHandler>,
}
struct ChannelHandler {
mask: i32,
proc: usize,
client_data: usize,
}
impl Channel {
fn readable(&self) -> bool {
self.mode & TCL_READABLE != 0
}
fn writable(&self) -> bool {
self.mode & TCL_WRITABLE != 0
}
}
#[derive(Default)]
struct Table {
channels: HashMap<usize, Channel>,
names: HashMap<String, usize>,
next_id: usize,
std: [Option<usize>; 3],
std_initialized: [bool; 3],
}
thread_local! {
static TABLE: RefCell<Table> = RefCell::new(Table::default());
}
fn with_table<T>(f: impl FnOnce(&mut Table) -> T) -> T {
TABLE.with(|t| f(&mut t.borrow_mut()))
}
fn std_index(kind: i32) -> Option<usize> {
match kind {
TCL_STDIN => Some(0),
TCL_STDOUT => Some(1),
TCL_STDERR => Some(2),
_ => None,
}
}
struct FileDevice {
file: File,
}
impl Device for FileDevice {
fn type_name(&self) -> &str {
"file"
}
fn read(&mut self, buf: &mut [u8]) -> Result<usize, String> {
self.file.read(buf).map_err(|e| errno_message(&e))
}
fn write(&mut self, buf: &[u8]) -> Result<usize, String> {
self.file.write(buf).map_err(|e| errno_message(&e))
}
fn seek(&mut self, offset: i64, whence: Whence) -> Result<i64, String> {
let to = match whence {
Whence::Start => SeekFrom::Start(offset.max(0) as u64),
Whence::Current => SeekFrom::Current(offset),
Whence::End => SeekFrom::End(offset),
};
self.file
.seek(to)
.map(|p| p as i64)
.map_err(|e| errno_message(&e))
}
fn seekable(&self) -> bool {
true
}
fn close(&mut self) -> Result<(), String> {
self.file.flush().map_err(|e| errno_message(&e))
}
fn handle(&self, _direction: i32) -> Option<isize> {
use std::os::fd::AsRawFd;
Some(self.file.as_raw_fd() as isize)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StdKind {
In,
Out,
Err,
}
struct StdDevice {
kind: StdKind,
}
impl Device for StdDevice {
fn type_name(&self) -> &str {
"file"
}
fn read(&mut self, buf: &mut [u8]) -> Result<usize, String> {
match self.kind {
StdKind::In => std::io::stdin().read(buf).map_err(|e| errno_message(&e)),
_ => Err("bad file descriptor".to_string()),
}
}
fn write(&mut self, buf: &[u8]) -> Result<usize, String> {
match self.kind {
StdKind::Out => Ok(buf.len()),
StdKind::Err => {
let mut err = std::io::stderr();
err.write_all(buf).map_err(|e| errno_message(&e))?;
let _ = err.flush();
Ok(buf.len())
}
StdKind::In => Err("bad file descriptor".to_string()),
}
}
fn close(&mut self) -> Result<(), String> {
Ok(())
}
fn handle(&self, _direction: i32) -> Option<isize> {
Some(match self.kind {
StdKind::In => 0,
StdKind::Out => 1,
StdKind::Err => 2,
})
}
}
fn errno_message(e: &std::io::Error) -> String {
let text = match e.raw_os_error() {
Some(code) => unsafe {
let p = libc::strerror(code);
std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
},
None => e.to_string(),
};
let mut chars = text.chars();
match chars.next() {
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
None => text,
}
}
pub fn create(name: &str, device: Box<dyn Device>, mode: i32) -> usize {
with_table(|t| {
let id = t.next_id;
t.next_id += 1;
t.names.insert(name.to_string(), id);
t.channels.insert(
id,
Channel {
name: name.to_string(),
device,
mode,
ref_count: 0,
input_translation: Translation::Auto,
output_translation: Translation::Lf,
encoding: Encoding::Utf8,
buffering: Buffering::Full,
buffer_size: DEFAULT_BUFFER_SIZE,
blocking: true,
raw: Vec::new(),
pending: String::new(),
device_eof: false,
saw_cr: false,
out: Vec::new(),
handlers: Vec::new(),
},
);
id
})
}
pub fn register(id: usize) {
with_table(|t| {
if let Some(c) = t.channels.get_mut(&id) {
c.ref_count += 1;
}
});
}
pub fn unregister(id: usize) -> Result<(), String> {
let should_close = with_table(|t| match t.channels.get_mut(&id) {
Some(c) => {
c.ref_count -= 1;
c.ref_count <= 0
}
None => false,
});
if should_close {
close_id(id)?;
}
Ok(())
}
pub fn name_of(id: usize) -> Option<String> {
with_table(|t| t.channels.get(&id).map(|c| c.name.clone()))
}
pub fn mode_of(id: usize) -> i32 {
with_table(|t| t.channels.get(&id).map_or(0, |c| c.mode))
}
pub fn handle_of(id: usize, direction: i32) -> Option<isize> {
with_table(|t| t.channels.get(&id).and_then(|c| c.device.handle(direction)))
}
pub fn set_std(kind: i32, id: Option<usize>) {
let Some(i) = std_index(kind) else { return };
with_table(|t| {
t.std[i] = id;
t.std_initialized[i] = true;
});
}
pub fn std_channel(kind: i32) -> Option<usize> {
let i = std_index(kind)?;
if with_table(|t| t.std_initialized[i]) {
return with_table(|t| t.std[i]);
}
let (name, device, mode, translation, buffering) = match kind {
TCL_STDIN => (
"stdin",
StdKind::In,
TCL_READABLE,
Translation::Auto,
Buffering::Full,
),
TCL_STDOUT => (
"stdout",
StdKind::Out,
TCL_WRITABLE,
Translation::Lf,
Buffering::Line,
),
_ => (
"stderr",
StdKind::Err,
TCL_WRITABLE,
Translation::Lf,
Buffering::None,
),
};
let id = create(name, Box::new(StdDevice { kind: device }), mode);
with_table(|t| {
if let Some(c) = t.channels.get_mut(&id) {
if mode & TCL_READABLE != 0 {
c.input_translation = translation;
} else {
c.output_translation = translation;
}
c.buffering = buffering;
}
t.std[i] = Some(id);
t.std_initialized[i] = true;
});
register(id);
Some(id)
}
struct Detached {
id: usize,
}
impl Device for Detached {
fn type_name(&self) -> &str {
"detached"
}
fn read(&mut self, _buf: &mut [u8]) -> Result<usize, String> {
Err("the channel's device is servicing an earlier call".to_string())
}
fn write(&mut self, buf: &[u8]) -> Result<usize, String> {
let id = self.id;
with_table(|t| {
if let Some(c) = t.channels.get_mut(&id) {
c.out.extend_from_slice(buf);
}
});
Ok(buf.len())
}
fn close(&mut self) -> Result<(), String> {
Ok(())
}
}
fn with_device_detached<T>(id: usize, f: impl FnOnce(&mut dyn Device) -> T) -> Option<T> {
let mut device = with_table(|t| {
t.channels
.get_mut(&id)
.map(|c| std::mem::replace(&mut c.device, Box::new(Detached { id })))
})?;
let out = f(device.as_mut());
with_table(|t| {
if let Some(c) = t.channels.get_mut(&id) {
c.device = device;
}
});
Some(out)
}
pub fn lookup(name: &str) -> Option<usize> {
if let Some(kind) = match name {
"stdin" => Some(TCL_STDIN),
"stdout" => Some(TCL_STDOUT),
"stderr" => Some(TCL_STDERR),
_ => None,
} {
if let Some(id) = std_channel(kind) {
return Some(id);
}
}
with_table(|t| t.names.get(name).copied())
}
fn resolve(name: &str) -> Result<usize, String> {
lookup(name).ok_or_else(|| format!("can not find channel named \"{name}\""))
}
fn close_id(id: usize) -> Result<(), String> {
let flushed = flush_id(id, None);
let device = with_table(|t| {
t.channels.remove(&id).map(|c| {
t.names.remove(&c.name);
for slot in t.std.iter_mut() {
if *slot == Some(id) {
*slot = None;
}
}
c.device
})
});
let outcome = match device {
Some(mut d) => d.close(),
None => Ok(()),
};
flushed.and(outcome)
}
fn fill(c: &mut Channel) -> Result<bool, String> {
if c.device_eof {
return Ok(false);
}
let mut buf = vec![0u8; c.buffer_size.clamp(1, 1 << 20) as usize];
let n = c.device.read(&mut buf)?;
if n == 0 {
c.device_eof = true;
c.saw_cr = false;
if !c.raw.is_empty() {
let tail = std::mem::take(&mut c.raw);
let text = String::from_utf8_lossy(&tail).into_owned();
translate_in(c, &text);
}
return Ok(false);
}
c.raw.extend_from_slice(&buf[..n]);
decode(c)?;
Ok(true)
}
fn decode(c: &mut Channel) -> Result<(), String> {
let text = match c.encoding {
Encoding::Named(name) => {
let (text, used) = crate::cmd_encoding::stream_decode(name, &c.raw)
.map_err(|e| format!("error reading \"{}\": {e}", c.name))?;
c.raw.drain(..used);
text
}
Encoding::Iso8859_1 => {
let s: String = c.raw.iter().map(|b| *b as char).collect();
c.raw.clear();
s
}
Encoding::Utf8 => {
let taken = std::mem::take(&mut c.raw);
match String::from_utf8(taken) {
Ok(s) => s,
Err(e) => {
let valid = e.utf8_error().valid_up_to();
let bytes = e.into_bytes();
let (good, rest) = bytes.split_at(valid);
let s = String::from_utf8_lossy(good).into_owned();
c.raw = rest.to_vec();
s
}
}
}
};
translate_in(c, &text);
Ok(())
}
fn translate_in(c: &mut Channel, text: &str) {
match c.input_translation {
Translation::Lf => c.pending.push_str(text),
Translation::Cr => {
for ch in text.chars() {
c.pending.push(if ch == '\r' { '\n' } else { ch });
}
}
Translation::Crlf => {
for ch in text.chars() {
if c.saw_cr {
c.saw_cr = false;
if ch == '\n' {
c.pending.push('\n');
continue;
}
c.pending.push('\r');
}
if ch == '\r' {
c.saw_cr = true;
} else {
c.pending.push(ch);
}
}
}
Translation::Auto => {
for ch in text.chars() {
if c.saw_cr {
c.saw_cr = false;
if ch == '\n' {
continue;
}
}
if ch == '\r' {
c.saw_cr = true;
c.pending.push('\n');
} else {
c.pending.push(ch);
}
}
}
}
}
fn gets_id(id: usize) -> Result<Option<String>, String> {
with_channel(id, |c| loop {
if let Some(at) = c.pending.find('\n') {
let line = c.pending[..at].to_string();
c.pending.drain(..=at);
return Ok(Some(line));
}
if !fill(c)? {
if c.pending.is_empty() {
return Ok(None);
}
return Ok(Some(std::mem::take(&mut c.pending)));
}
})
}
fn read_id(id: usize, count: Option<i64>) -> Result<String, String> {
with_channel(id, |c| match count {
None => {
while fill(c)? {}
Ok(std::mem::take(&mut c.pending))
}
Some(n) if n <= 0 => Ok(String::new()),
Some(n) => {
let n = n as usize;
while c.pending.chars().count() < n {
if !fill(c)? {
break;
}
}
let end = c
.pending
.char_indices()
.nth(n)
.map_or(c.pending.len(), |(i, _)| i);
let taken = c.pending[..end].to_string();
c.pending.drain(..end);
Ok(taken)
}
})
}
fn eof_id(id: usize) -> Result<bool, String> {
with_channel(id, |c| Ok(c.device_eof && c.pending.is_empty()))
}
fn translate_out(t: Translation, text: &str) -> String {
match t {
Translation::Lf | Translation::Auto => text.to_string(),
Translation::Cr => text.replace('\n', "\r"),
Translation::Crlf => text.replace('\n', "\r\n"),
}
}
fn encode(e: Encoding, text: &str) -> Result<Vec<u8>, String> {
Ok(match e {
Encoding::Utf8 => text.as_bytes().to_vec(),
Encoding::Named(name) => crate::cmd_encoding::stream_encode(name, text)?,
Encoding::Iso8859_1 => text
.chars()
.map(|ch| if (ch as u32) < 0x100 { ch as u8 } else { b'?' })
.collect(),
})
}
fn write_id(id: usize, text: &str, sink: Option<&Output>) -> Result<(), String> {
let to_sink = with_table(|t| {
t.channels
.get(&id)
.is_some_and(|c| c.name == "stdout" && c.device.type_name() == "file")
});
if to_sink {
if let Some(out) = sink {
let translated = with_channel(id, |c| Ok(translate_out(c.output_translation, text)))?;
out.write(&translated);
return Ok(());
}
}
let ready = with_channel(id, |c| {
let translated = translate_out(c.output_translation, text);
let name = c.name.clone();
c.out.extend_from_slice(
&encode(c.encoding, &translated)
.map_err(|e| format!("error writing \"{name}\": {e}"))?,
);
Ok(match c.buffering {
Buffering::None => true,
Buffering::Line => translated.contains('\n'),
Buffering::Full => c.out.len() as i64 >= c.buffer_size,
})
})?;
if ready {
flush_id(id, sink)?;
}
Ok(())
}
fn flush_id(id: usize, sink: Option<&Output>) -> Result<(), String> {
let pending = with_table(|t| {
t.channels
.get_mut(&id)
.map(|c| std::mem::take(&mut c.out))
.unwrap_or_default()
});
if pending.is_empty() {
if let (Some(out), true) = (sink, name_of(id).as_deref() == Some("stdout")) {
out.flush();
}
return Ok(());
}
with_device_detached(id, |device| {
let mut at = 0;
while at < pending.len() {
match device.write(&pending[at..]) {
Ok(0) => return Err("channel is not writable".to_string()),
Ok(n) => at += n,
Err(e) => return Err(e),
}
}
Ok(())
})
.unwrap_or(Ok(()))
}
fn with_channel<T>(
id: usize,
f: impl FnOnce(&mut Channel) -> Result<T, String>,
) -> Result<T, String> {
with_table(|t| match t.channels.get_mut(&id) {
Some(c) => f(c),
None => Err("channel is not open".to_string()),
})
}
const GENERIC_OPTIONS: &[&str] = &[
"-blocking",
"-buffering",
"-buffersize",
"-encoding",
"-eofchar",
"-profile",
"-translation",
];
pub fn get_option(id: usize, option: &str) -> Result<String, String> {
with_channel(id, |c| match option {
"-blocking" => Ok(if c.blocking { "1" } else { "0" }.to_string()),
"-buffering" => Ok(c.buffering.name().to_string()),
"-buffersize" => Ok(c.buffer_size.to_string()),
"-encoding" => Ok(c.encoding.name().to_string()),
"-eofchar" => Ok(String::new()),
"-profile" => Ok("strict".to_string()),
"-translation" => Ok(match (c.readable(), c.writable()) {
(true, true) => format!(
"{} {}",
c.input_translation.name(),
c.output_translation.name()
),
(true, false) => c.input_translation.name().to_string(),
_ => c.output_translation.name().to_string(),
}),
other => Err(bad_option(other)),
})
}
fn bad_option(name: &str) -> String {
format!(
"bad option \"{name}\": should be one of -blocking, -buffering, \
-buffersize, -encoding, -eofchar, -profile, -translation, or -stat"
)
}
pub fn set_option(id: usize, option: &str, value: &str) -> Result<(), String> {
match option {
"-translation" => set_translation(id, value),
"-encoding" => {
let encoding = match value {
"utf-8" | "utf8" => Encoding::Utf8,
"iso8859-1" | "iso-8859-1" | "latin1" => Encoding::Iso8859_1,
"" | "binary" => {
return Err(format!(
"unknown encoding \"{value}\": No longer supported.\n\
\tplease use either \"-translation binary\" or \
\"-encoding iso8859-1\""
))
}
other => match crate::cmd_encoding::static_name(other) {
Some(name) => Encoding::Named(name),
None => return Err(format!("unknown encoding \"{other}\"")),
},
};
with_channel(id, |c| {
c.encoding = encoding;
Ok(())
})
}
"-buffering" => {
let buffering = match value {
"full" => Buffering::Full,
"line" => Buffering::Line,
"none" => Buffering::None,
_ => {
return Err(
"bad value for -buffering: must be one of full, line, or none".to_string(),
)
}
};
with_channel(id, |c| {
c.buffering = buffering;
Ok(())
})
}
"-buffersize" => {
let size: i64 = value
.parse()
.map_err(|_| format!("expected integer but got \"{value}\""))?;
with_channel(id, |c| {
c.buffer_size = size.clamp(1, 1 << 20);
Ok(())
})
}
"-blocking" => match value {
"1" | "yes" | "true" | "on" => with_channel(id, |c| {
c.blocking = true;
Ok(())
}),
"0" | "no" | "false" | "off" => Err(
"non-blocking channels are not implemented in this frontend; \
-blocking 0 is refused rather than accepted and ignored"
.to_string(),
),
other => Err(format!("expected boolean value but got \"{other}\"")),
},
"-eofchar" | "-profile" => Err(format!(
"{option} is not implemented in this frontend; it is reported at its \
default and refused when set"
)),
other => Err(bad_option(other)),
}
}
fn set_translation(id: usize, value: &str) -> Result<(), String> {
let parts = crate::list::split(value)
.map_err(|_| "bad value for -translation: must be a one or two element list".to_string())?;
let (read_mode, write_mode) = match parts.len() {
1 => (parts[0].as_str(), parts[0].as_str()),
2 => (parts[0].as_str(), parts[1].as_str()),
_ => {
return Err("bad value for -translation: must be a one or two element list".to_string())
}
};
let readable = with_channel(id, |c| Ok(c.readable()))?;
let writable = with_channel(id, |c| Ok(c.writable()))?;
if readable {
let (translation, binary) = parse_translation(read_mode, Translation::Auto)?;
with_channel(id, |c| {
c.input_translation = translation;
if binary {
c.encoding = Encoding::Iso8859_1;
}
Ok(())
})?;
}
if writable {
let (translation, binary) = parse_translation(write_mode, Translation::Lf)?;
with_channel(id, |c| {
c.output_translation = translation;
if binary {
c.encoding = Encoding::Iso8859_1;
}
Ok(())
})?;
}
Ok(())
}
fn parse_translation(word: &str, auto: Translation) -> Result<(Translation, bool), String> {
Ok(match word {
"auto" => (auto, false),
"binary" => (Translation::Lf, true),
"lf" => (Translation::Lf, false),
"cr" => (Translation::Cr, false),
"crlf" => (Translation::Crlf, false),
"platform" => (Translation::Lf, false),
_ => {
return Err(
"bad value for -translation: must be one of auto, binary, cr, lf, \
crlf, or platform"
.to_string(),
)
}
})
}
pub fn adopt_empty_std_slot(id: usize) {
let taken = with_table(|t| {
for (i, name) in ["stdin", "stdout", "stderr"].iter().enumerate() {
if t.std[i].is_none() && t.std_initialized[i] {
if let Some(c) = t.channels.get_mut(&id) {
t.names.remove(&c.name);
c.name = (*name).to_string();
t.names.insert((*name).to_string(), id);
}
t.std[i] = Some(id);
return true;
}
}
false
});
if taken {
register(id);
}
}
pub fn write_bytes(id: usize, bytes: &[u8]) -> Result<(), String> {
let text = String::from_utf8_lossy(bytes).into_owned();
write_id(id, &text, None)
}
pub fn read_chars(id: usize, count: Option<i64>) -> Result<String, String> {
read_id(id, count)
}
pub fn gets(id: usize) -> Result<Option<String>, String> {
gets_id(id)
}
pub fn flush(id: usize) -> Result<(), String> {
flush_id(id, None)
}
pub fn close(id: usize) -> Result<(), String> {
close_id(id)
}
pub fn half_close(id: usize, flags: i32) -> Result<(), String> {
let dropping = flags & (TCL_READABLE | TCL_WRITABLE);
let left = with_channel(id, |c| {
c.mode &= !dropping;
Ok(c.mode & (TCL_READABLE | TCL_WRITABLE))
})?;
if left == 0 {
return close_id(id);
}
Ok(())
}
pub fn seek(id: usize, offset: i64, whence: Whence) -> Result<i64, String> {
seek_at(id, offset, whence)
}
pub fn tell(id: usize) -> Result<i64, String> {
with_channel(id, |c| {
if !c.device.seekable() {
return Ok(-1);
}
let at = c.device.seek(0, Whence::Current)?;
Ok(at - c.pending.chars().count() as i64 - c.raw.len() as i64)
})
}
pub fn at_eof(id: usize) -> bool {
eof_id(id).unwrap_or(false)
}
pub fn input_buffered(id: usize) -> usize {
with_channel(id, |c| Ok(c.pending.len() + c.raw.len())).unwrap_or(0)
}
pub fn output_buffered(id: usize) -> usize {
with_channel(id, |c| Ok(c.out.len())).unwrap_or(0)
}
pub fn ref_count(id: usize) -> isize {
with_table(|t| t.channels.get(&id).map_or(0, |c| c.ref_count))
}
pub fn buffer_size(id: usize) -> i64 {
with_channel(id, |c| Ok(c.buffer_size)).unwrap_or(DEFAULT_BUFFER_SIZE)
}
pub fn set_buffer_size(id: usize, size: i64) {
let _ = with_channel(id, |c| {
c.buffer_size = size.clamp(1, 1 << 20);
Ok(())
});
}
pub fn set_channel_option(id: usize, option: &str, value: &str) -> Result<(), String> {
match set_option(id, option, value) {
Err(e) if e.starts_with("bad option ") => {
match with_channel(id, |c| Ok(c.device.set_option(option, value)))? {
Some(answer) => answer,
None => Err(e),
}
}
other => other,
}
}
pub fn get_channel_option(id: usize, option: &str) -> Result<String, String> {
match get_option(id, option) {
Err(e) if e.starts_with("bad option ") => {
match with_channel(id, |c| Ok(c.device.get_option(option)))? {
Some(answer) => answer,
None => Err(e),
}
}
other => other,
}
}
pub fn all_options(id: usize) -> Result<String, String> {
let mut parts = Vec::with_capacity(GENERIC_OPTIONS.len() * 2);
for option in GENERIC_OPTIONS {
parts.push((*option).to_string());
parts.push(get_option(id, option)?);
}
Ok(crate::list::join(&parts))
}
pub fn driver_table(id: usize) -> Option<usize> {
with_channel(id, |c| Ok(c.device.driver_table())).unwrap_or(None)
}
pub fn instance_data(id: usize) -> Option<usize> {
with_channel(id, |c| Ok(c.device.instance_data())).unwrap_or(None)
}
pub fn open_file(path: &str, access: &str) -> Result<String, String> {
open(path, Some(access))
}
pub fn create_channel_handler(id: usize, mask: i32, proc: usize, client_data: usize) {
let interest = with_table(|t| {
let Some(c) = t.channels.get_mut(&id) else {
return 0;
};
c.handlers
.retain(|h| !(h.proc == proc && h.client_data == client_data));
if mask != 0 {
c.handlers.push(ChannelHandler {
mask,
proc,
client_data,
});
}
c.handlers.iter().fold(0, |acc, h| acc | h.mask)
});
with_device_detached(id, |device| device.watch(interest));
}
pub fn delete_channel_handler(id: usize, proc: usize, client_data: usize) {
create_channel_handler(id, 0, proc, client_data);
}
pub fn handlers_for(id: usize, mask: i32) -> Vec<(usize, usize, i32)> {
with_table(|t| {
t.channels
.get(&id)
.map(|c| {
c.handlers
.iter()
.filter(|h| h.mask & mask != 0)
.map(|h| (h.proc, h.client_data, h.mask & mask))
.collect()
})
.unwrap_or_default()
})
}
pub const COMMANDS: &[&str] = &[
"open",
"close",
"gets",
"read",
"flush",
"eof",
"seek",
"tell",
"fconfigure",
];
pub(crate) fn compile(c: &mut Compiler, name: &str, args: &[Word]) -> Result<(), CompileError> {
let (id, usage, min, max) = match name {
"open" => (ext::OPEN, "open fileName ?access? ?permissions?", 1, 3),
"close" => (ext::CLOSE, "close channel ?direction?", 1, 2),
"read" => (
ext::READ,
"read channel ?numChars?\" or \"read ?-nonewline? channel",
1,
2,
),
"flush" => (ext::FLUSH, "flush channel", 1, 1),
"eof" => (ext::EOF, "eof channel", 1, 1),
"seek" => (ext::SEEK, "seek channel offset ?origin?", 2, 3),
"tell" => (ext::TELL, "tell channel", 1, 1),
"fconfigure" => (
ext::FCONFIGURE,
"fconfigure channel ?-option value ...?",
1,
usize::MAX,
),
"gets" => return compile_gets(c, args),
other => return c.error(format!("invalid command name \"{other}\"")),
};
if args.len() < min || args.len() > max {
return c.error(format!("wrong # args: should be \"{usage}\""));
}
for arg in args {
c.word(arg)?;
}
let argc = u8::try_from(args.len()).map_err(|_| c.err("too many arguments for one command"))?;
c.emit(Op::Extended(id, argc), 1 - args.len() as i32);
Ok(())
}
fn compile_gets(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let (channel, var) = match args {
[channel] => (channel, None),
[channel, var] => (channel, Some(var)),
_ => return c.error("wrong # args: should be \"gets channel ?varName?\""),
};
c.word(channel)?;
let operands = match var {
None => 1,
Some(word) => {
let name = c.var_name_of(word)?;
let encoded = c.place_operand(&name);
c.emit(Op::LoadInt(encoded), 1);
2
}
};
c.emit(Op::Extended(ext::GETS, operands), 1 - operands as i32);
Ok(())
}
pub(crate) fn compile_puts(
c: &mut Compiler,
channel: &Word,
value: &Word,
newline: bool,
) -> Result<(), CompileError> {
c.word(channel)?;
c.word(value)?;
c.emit(Op::Extended(ext::CH_PUTS, u8::from(newline)), -1);
Ok(())
}
pub(crate) fn run(vm: &mut VM, id: u16, arg: u8, sink: &Output) -> Result<(), String> {
let count = if id == ext::CH_PUTS { 2 } else { arg as usize };
let mut operands = Vec::with_capacity(count);
for _ in 0..count {
operands.push(vm.pop());
}
operands.reverse();
let text = |i: usize| to_tcl_string(operands.get(i).unwrap_or(&Value::Undef));
let result = match id {
ext::OPEN => Value::Str(Arc::new(open(
&text(0),
operands.get(1).map(|_| text(1)).as_deref(),
)?)),
ext::CLOSE => {
close_command(&text(0), operands.get(1).map(|_| text(1)).as_deref())?;
empty()
}
ext::GETS => {
let channel = resolve_readable(&text(0))?;
let line = gets_id(channel)?;
match operands.get(1) {
None => Value::Str(Arc::new(line.unwrap_or_default())),
Some(place) => {
let value = line.clone().unwrap_or_default();
assign(vm, place, &value)?;
Value::Int(match line {
Some(l) => l.chars().count() as i64,
None => -1,
})
}
}
}
ext::READ => read_command(operands.len(), &text)?,
ext::CH_PUTS => {
let channel = resolve_writable(&text(0))?;
let mut out = text(1);
if arg == 1 {
out.push('\n');
}
write_id(channel, &out, Some(sink))?;
empty()
}
ext::FLUSH => {
let channel = resolve_writable(&text(0))?;
flush_id(channel, Some(sink))?;
empty()
}
ext::EOF => Value::Int(i64::from(eof_id(resolve(&text(0))?)?)),
ext::SEEK => {
seek_command(&text(0), &text(1), operands.get(2).map(|_| text(2)))?;
empty()
}
ext::TELL => Value::Int(tell_command(&text(0))?),
ext::FCONFIGURE => {
let rest: Vec<String> = (1..operands.len()).map(text).collect();
Value::Str(Arc::new(fconfigure(&text(0), &rest)?))
}
other => return Err(format!("unknown channel op {other}")),
};
vm.push(result);
Ok(())
}
fn empty() -> Value {
Value::Str(Arc::new(String::new()))
}
fn assign(vm: &mut VM, encoded: &Value, value: &str) -> Result<(), String> {
let raw = match encoded {
Value::Int(v) => *v,
other => return Err(format!("gets: not a variable place: {other:?}")),
};
let place = place_at(&Value::Int(raw >> 1), raw & 1 == 1)?;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::new(value.to_string()));
}
Ok(())
}
fn resolve_readable(name: &str) -> Result<usize, String> {
let id = resolve(name)?;
if !with_channel(id, |c| Ok(c.readable()))? {
return Err(format!("channel \"{name}\" wasn't opened for reading"));
}
Ok(id)
}
fn resolve_writable(name: &str) -> Result<usize, String> {
let id = resolve(name)?;
if !with_channel(id, |c| Ok(c.writable()))? {
return Err(format!("channel \"{name}\" wasn't opened for writing"));
}
Ok(id)
}
fn open(path: &str, access: Option<&str>) -> Result<String, String> {
if path.starts_with('|') {
return Err(
"opening a command pipeline is not implemented in this frontend; \
open refuses it rather than opening a file named \"|…\""
.to_string(),
);
}
let access = access.unwrap_or("r");
let mut options = std::fs::OpenOptions::new();
match access {
"r" => options.read(true),
"r+" => options.read(true).write(true),
"w" => options.write(true).create(true).truncate(true),
"w+" => options.read(true).write(true).create(true).truncate(true),
"a" => options.append(true).create(true),
"a+" => options.read(true).write(true).create(true),
other => {
if other.contains(char::is_whitespace)
|| other.chars().all(|c| c.is_ascii_uppercase() || c == '_')
{
return Err(format!(
"the POSIX list form of an access mode ({other}) is not \
implemented in this frontend; the r/w/a strings are"
));
}
return Err(format!("illegal access mode \"{other}\""));
}
};
let mut file = options
.open(path)
.map_err(|e| format!("couldn't open \"{path}\": {}", errno_message(&e)))?;
if access.starts_with('a') {
file.seek(SeekFrom::End(0)).map_err(|e| errno_message(&e))?;
}
let mode = match access {
"r" => TCL_READABLE,
"w" | "a" => TCL_WRITABLE,
_ => TCL_READABLE | TCL_WRITABLE,
};
let name = {
use std::os::fd::AsRawFd;
format!("file{}", file.as_raw_fd())
};
let id = create(&name, Box::new(FileDevice { file }), mode);
register(id);
Ok(name)
}
fn close_command(name: &str, direction: Option<&str>) -> Result<(), String> {
let id = resolve(name)?;
if let Some(direction) = direction {
let (want, side) = match direction {
"read" | "r" => (TCL_READABLE, "read"),
"write" | "w" => (TCL_WRITABLE, "write"),
other => return Err(format!("bad direction \"{other}\": must be read or write")),
};
let mode = mode_of(id);
if mode & want == 0 {
return Err(format!(
"Half-close of {side}-side not possible, side not opened or already closed"
));
}
if mode & !want & (TCL_READABLE | TCL_WRITABLE) != 0 {
return Err(format!(
"half-closing the {side} side of a read-write channel is not \
implemented in this frontend; no device here has a close2Proc \
that honours it"
));
}
}
unregister(id)
}
fn read_command(argc: usize, text: &impl Fn(usize) -> String) -> Result<Value, String> {
let (name, count_word, nonewline) = match argc {
1 => (text(0), None, false),
2 if text(0) == "-nonewline" => (text(1), None, true),
2 => (text(0), Some(text(1)), false),
_ => {
return Err("wrong # args: should be \"read channel ?numChars?\" or \
\"read ?-nonewline? channel\""
.to_string())
}
};
let id = resolve_readable(&name)?;
let count = match count_word {
None => None,
Some(n) => Some(
n.parse::<i64>()
.ok()
.filter(|v| *v >= 0)
.ok_or_else(|| format!("expected non-negative integer but got \"{n}\""))?,
),
};
let mut out = read_id(id, count)?;
if nonewline && out.ends_with('\n') {
out.pop();
}
Ok(Value::Str(Arc::new(out)))
}
fn seek_command(name: &str, offset: &str, origin: Option<String>) -> Result<(), String> {
let id = resolve(name)?;
let offset: i64 = offset
.parse()
.map_err(|_| format!("expected integer but got \"{offset}\""))?;
let whence = match origin.as_deref().unwrap_or("start") {
"start" => Whence::Start,
"current" => Whence::Current,
"end" => Whence::End,
other => {
return Err(format!(
"bad origin \"{other}\": must be start, current, or end"
))
}
};
seek_at(id, offset, whence)
.map(|_| ())
.map_err(|e| match e.as_str() {
"illegal seek" => format!("error during seek on \"{name}\": illegal seek"),
_ => e,
})
}
fn seek_at(id: usize, offset: i64, whence: Whence) -> Result<i64, String> {
flush_id(id, None)?;
with_channel(id, |c| {
if !c.device.seekable() {
return Err("illegal seek".to_string());
}
let buffered = c.pending.chars().count() as i64 + c.raw.len() as i64;
let offset = if whence == Whence::Current {
offset - buffered
} else {
offset
};
let at = c.device.seek(offset, whence)?;
c.pending.clear();
c.raw.clear();
c.device_eof = false;
c.saw_cr = false;
Ok(at)
})
}
fn tell_command(name: &str) -> Result<i64, String> {
let id = resolve(name)?;
with_channel(id, |c| {
if !c.device.seekable() {
return Ok(-1);
}
let at = c.device.seek(0, Whence::Current)?;
Ok(at - c.pending.chars().count() as i64 - c.raw.len() as i64)
})
}
fn fconfigure(name: &str, rest: &[String]) -> Result<String, String> {
let id = resolve(name)?;
match rest.len() {
0 => {
let mut parts = Vec::with_capacity(GENERIC_OPTIONS.len() * 2);
for option in GENERIC_OPTIONS {
parts.push((*option).to_string());
parts.push(get_option(id, option)?);
}
Ok(crate::list::join(&parts))
}
1 => get_option(id, &rest[0]),
n if n % 2 == 0 => {
for pair in rest.chunks(2) {
set_option(id, &pair[0], &pair[1])?;
}
Ok(String::new())
}
_ => Err("wrong # args: should be \"fconfigure channel ?-option value ...?\"".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_closed_channel_gives_its_name_back() {
let path = std::env::temp_dir().join(format!("tclrs-chan-name-{}", std::process::id()));
std::fs::write(&path, b"x").expect("write");
let name = open(path.to_str().expect("path"), None).expect("open");
assert!(lookup(&name).is_some());
close_command(&name, None).expect("close");
assert!(lookup(&name).is_none());
let _ = std::fs::remove_file(&path);
}
#[test]
fn auto_translation_joins_a_split_carriage_return() {
let mut c = Channel {
name: "test".to_string(),
device: Box::new(StdDevice { kind: StdKind::In }),
mode: TCL_READABLE,
ref_count: 0,
input_translation: Translation::Auto,
output_translation: Translation::Lf,
encoding: Encoding::Utf8,
buffering: Buffering::Full,
buffer_size: DEFAULT_BUFFER_SIZE,
blocking: true,
raw: Vec::new(),
pending: String::new(),
device_eof: false,
saw_cr: false,
out: Vec::new(),
handlers: Vec::new(),
};
translate_in(&mut c, "a\r");
translate_in(&mut c, "\nb\rc\nd");
assert_eq!(c.pending, "a\nb\nc\nd");
}
}