use super::MMix;
use std::fs::{File, OpenOptions};
use std::io::{ErrorKind, Read, Seek, SeekFrom, Write};
use tracing::debug;
const TRANSFER_CHUNK: usize = 1_048_576;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum TrapCode {
Halt = 0,
Fopen = 1,
Fclose = 2,
Fread = 3,
Fgets = 4,
Fgetws = 5,
Fwrite = 6,
Fputs = 7,
Fputws = 8,
Fseek = 9,
Ftell = 10,
Fputc = 0x80,
Time = 0x81,
Debug = 0x82,
}
impl TrapCode {
pub fn from_u8(n: u8) -> Option<Self> {
match n {
0 => Some(TrapCode::Halt),
1 => Some(TrapCode::Fopen),
2 => Some(TrapCode::Fclose),
3 => Some(TrapCode::Fread),
4 => Some(TrapCode::Fgets),
5 => Some(TrapCode::Fgetws),
6 => Some(TrapCode::Fwrite),
7 => Some(TrapCode::Fputs),
8 => Some(TrapCode::Fputws),
9 => Some(TrapCode::Fseek),
10 => Some(TrapCode::Ftell),
0x80 => Some(TrapCode::Fputc),
0x81 => Some(TrapCode::Time),
0x82 => Some(TrapCode::Debug),
_ => None,
}
}
}
pub(super) struct FileHandle {
pub(super) file: Option<File>,
pub(super) read: bool,
pub(super) write: bool,
pub(super) seek: bool,
pub(super) read_write: bool,
}
impl MMix {
pub(super) fn handle_trap(&mut self, trap_code: TrapCode, arg: u8) -> bool {
let arg255 = self.get_register(255);
let result = match trap_code {
TrapCode::Halt => self.handle_halt(arg),
TrapCode::Fopen => self.handle_fopen(arg),
TrapCode::Fclose => self.handle_fclose(arg),
TrapCode::Fread => self.handle_fread(arg),
TrapCode::Fgets => self.handle_fgets(arg),
TrapCode::Fgetws => self.handle_fgetws(arg),
TrapCode::Fwrite => self.handle_fwrite(arg),
TrapCode::Fputs => self.handle_fputs(arg),
TrapCode::Fputc => self.handle_fputc(arg),
TrapCode::Fputws => self.handle_fputws(arg),
TrapCode::Fseek => self.handle_fseek(arg),
TrapCode::Ftell => self.handle_ftell(arg),
TrapCode::Time => self.handle_time(arg),
TrapCode::Debug => self.handle_debug(arg),
};
let result255 = self.get_register(255);
self.host.trap(trap_code, arg, arg255, result255);
result
}
fn handle_halt(&mut self, _arg: u8) -> bool {
debug!("TRAP: Halt");
let exit_code = self.get_register(255);
self.exit_code = exit_code;
self.host.flush();
self.host.diagnostic(&format!(
"HALT trap at PC={:#018x}, exit code={}",
self.pc, exit_code
));
self.advance_pc();
false
}
fn handle_readable(&self, handle: u8) -> bool {
self.file_handles.get(&handle).is_some_and(|h| h.read)
}
fn handle_writable(&self, handle: u8) -> bool {
self.file_handles.get(&handle).is_some_and(|h| h.write)
}
fn handle_seekable(&self, handle: u8) -> bool {
self.file_handles.get(&handle).is_some_and(|h| h.seek)
}
fn read_from_handle(&mut self, handle: u8, buf: &mut [u8]) -> std::io::Result<usize> {
if handle == 0 {
self.host.read(0, buf)
} else {
self.open_file(handle).read(buf)
}
}
fn read_wyde_from_handle(&mut self, handle: u8) -> Option<[u8; 2]> {
let mut wyde = [0u8; 2];
let mut total = 0;
while total < wyde.len() {
match self.read_from_handle(handle, &mut wyde[total..]) {
Ok(0) => return None,
Ok(n) => total += n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(_) => return None,
}
}
Some(wyde)
}
fn fail_unless(&mut self, ok: bool, failure: i64) -> bool {
if !ok {
self.set_register(255, failure as u64);
self.advance_pc();
}
!ok
}
fn note_read(&mut self, handle: u8) {
if let Some(entry) = self.file_handles.get_mut(&handle)
&& entry.read_write
{
entry.write = false;
}
}
fn note_write(&mut self, handle: u8) {
if let Some(entry) = self.file_handles.get_mut(&handle)
&& entry.read_write
{
entry.read = false;
}
}
fn note_seek(&mut self, handle: u8) {
if let Some(entry) = self.file_handles.get_mut(&handle)
&& entry.read_write
{
entry.read = true;
entry.write = true;
}
}
fn fail_fopen(&mut self, handle: u8) -> bool {
self.file_handles.remove(&handle);
self.set_register(255, (-1i64) as u64);
self.advance_pc();
true
}
fn read_fopen_name(&self, handle: u8, name_addr: u64) -> Option<String> {
let mut name_bytes = Vec::new();
let mut cur = name_addr;
loop {
let byte = self.read_byte(cur);
if byte == 0 {
break;
}
name_bytes.push(byte);
cur = cur.wrapping_add(1);
}
match String::from_utf8(name_bytes) {
Ok(filename) => Some(filename),
Err(_) => {
debug!(handle, "TRAP: Fopen name is not valid UTF-8");
None
}
}
}
fn handle_fopen(&mut self, handle: u8) -> bool {
if handle <= 2 {
debug!(handle, "TRAP: Fopen rejects a standard handle");
self.set_register(255, (-1i64) as u64);
self.advance_pc();
return true;
}
let param_addr = self.get_register(255);
let name_addr = self.read_octa(param_addr);
let mode_octa = self.read_octa(param_addr.wrapping_add(8));
let Some(filename) = self.read_fopen_name(handle, name_addr) else {
return self.fail_fopen(handle);
};
debug!(handle, filename = %filename, mode = mode_octa, "TRAP: Fopen");
if mode_octa > 4 {
debug!(mode = mode_octa, "Invalid file open mode");
return self.fail_fopen(handle);
}
let mode = mode_octa as u8;
let caps = match mode {
0 => (true, false, false, false),
1 => (false, true, false, false),
2 => (true, false, true, false),
3 => (false, true, true, false),
_ => (true, true, true, true), };
self.file_handles.remove(&handle);
let opened = match mode {
0 | 2 => OpenOptions::new().read(true).open(&filename),
1 | 3 => OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&filename),
_ => OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&filename),
};
match opened {
Ok(file) => {
let (read, write, seek, read_write) = caps;
self.file_handles.insert(
handle,
FileHandle {
file: Some(file),
read,
write,
seek,
read_write,
},
);
self.set_register(255, 0);
debug!(handle, "File opened successfully");
self.advance_pc();
true
}
Err(_) => {
debug!(handle, "File open failed");
self.fail_fopen(handle)
}
}
}
fn handle_fclose(&mut self, handle: u8) -> bool {
debug!(handle, "TRAP: Fclose");
if handle <= 2 {
self.set_register(255, (-1i64) as u64);
self.advance_pc();
return true;
}
match self.file_handles.remove(&handle) {
Some(_) => self.set_register(255, 0),
None => self.set_register(255, (-1i64) as u64),
}
self.advance_pc();
true
}
fn handle_fread(&mut self, handle: u8) -> bool {
let param_addr = self.get_register(255);
let buffer_addr = self.read_octa(param_addr);
let size = self.read_octa(param_addr.wrapping_add(8));
debug!(
handle,
buffer_addr = format!("0x{:X}", buffer_addr),
size,
"TRAP: Fread"
);
if self.fail_unless(
self.handle_readable(handle),
u64::MAX.wrapping_sub(size) as i64,
) {
return true;
}
self.note_read(handle);
let chunk_size = size.min(TRANSFER_CHUNK as u64) as usize;
let mut chunk = vec![0u8; chunk_size];
let mut total: u64 = 0;
let mut had_error = false;
while total < size {
let want = (size - total).min(chunk_size as u64) as usize;
match self.read_from_handle(handle, &mut chunk[..want]) {
Ok(0) => break,
Ok(n) => {
for (i, &byte) in chunk[..n].iter().enumerate() {
self.write_byte(
buffer_addr.wrapping_add(total.wrapping_add(i as u64)),
byte,
);
}
total += n as u64;
}
Err(_) => {
had_error = true;
break;
}
}
}
let result = if had_error && total == 0 {
u64::MAX.wrapping_sub(size)
} else {
total.wrapping_sub(size)
};
self.set_register(255, result);
self.advance_pc();
true
}
fn handle_fgets(&mut self, handle: u8) -> bool {
let param_addr = self.get_register(255);
let buffer_addr = self.read_octa(param_addr);
let max_size = self.read_octa(param_addr.wrapping_add(8)) as usize;
debug!(
handle,
buffer_addr = format!("0x{:X}", buffer_addr),
max_size,
"TRAP: Fgets"
);
let ok = max_size != 0 && self.handle_readable(handle);
if self.fail_unless(ok, -1) {
return true;
}
self.note_read(handle);
let mut count = 0usize;
while count < max_size - 1 {
let mut byte = [0u8; 1];
match self.read_from_handle(handle, &mut byte) {
Ok(0) => break,
Ok(_) => {
self.write_byte(buffer_addr.wrapping_add(count as u64), byte[0]);
count += 1;
if byte[0] == b'\n' {
break;
}
}
Err(_) => break,
}
}
self.write_byte(buffer_addr.wrapping_add(count as u64), 0);
self.set_register(
255,
if count == 0 {
(-1i64) as u64
} else {
count as u64
},
);
self.advance_pc();
true
}
fn handle_fgetws(&mut self, handle: u8) -> bool {
let param_addr = self.get_register(255);
let buffer_addr = self.read_octa(param_addr) & !1u64;
let max_wydes = self.read_octa(param_addr.wrapping_add(8)) as usize;
debug!(
handle,
buffer_addr = format!("0x{:X}", buffer_addr),
max_wydes,
"TRAP: Fgetws"
);
let ok = max_wydes != 0 && self.handle_readable(handle);
if self.fail_unless(ok, -1) {
return true;
}
self.note_read(handle);
let mut count = 0usize;
while count < max_wydes - 1 {
match self.read_wyde_from_handle(handle) {
Some(wyde) => {
self.write_byte(buffer_addr.wrapping_add((count * 2) as u64), wyde[0]);
self.write_byte(buffer_addr.wrapping_add((count * 2 + 1) as u64), wyde[1]);
count += 1;
if wyde == [0x00, 0x0A] {
break;
}
}
None => break,
}
}
self.write_byte(buffer_addr.wrapping_add((count * 2) as u64), 0);
self.write_byte(buffer_addr.wrapping_add((count * 2 + 1) as u64), 0);
self.set_register(
255,
if count == 0 {
(-1i64) as u64
} else {
count as u64
},
);
self.advance_pc();
true
}
fn handle_fwrite(&mut self, handle: u8) -> bool {
let param_addr = self.get_register(255);
let buffer_addr = self.read_octa(param_addr);
let size = self.read_octa(param_addr.wrapping_add(8));
debug!(
handle,
buffer_addr = format!("0x{:X}", buffer_addr),
size,
"TRAP: Fwrite"
);
if self.fail_unless(self.handle_writable(handle), 0u64.wrapping_sub(size) as i64) {
return true;
}
self.note_write(handle);
let chunk_size = size.min(TRANSFER_CHUNK as u64) as usize;
let mut chunk = vec![0u8; chunk_size];
let mut written: u64 = 0;
loop {
let want = (size - written).min(chunk_size as u64) as usize;
let chunk_addr = buffer_addr.wrapping_add(written);
for (i, slot) in chunk[..want].iter_mut().enumerate() {
*slot = self.read_byte(chunk_addr.wrapping_add(i as u64));
}
let landed = self.write_counted(handle, &chunk[..want]);
written += landed as u64;
if landed < want || written == size {
break;
}
}
self.set_register(255, written.wrapping_sub(size));
self.advance_pc();
true
}
fn write_counted(&mut self, handle: u8, bytes: &[u8]) -> usize {
match handle {
1 | 2 => match self.host.write(handle, bytes) {
Ok(()) => bytes.len(),
Err(_) => 0,
},
_ => {
let file = self.open_file(handle);
let mut total = 0usize;
while total < bytes.len() {
match file.write(&bytes[total..]) {
Ok(0) | Err(_) => break,
Ok(n) => total += n,
}
}
total
}
}
}
fn open_file(&mut self, handle: u8) -> &mut File {
self.file_handles
.get_mut(&handle)
.and_then(|entry| entry.file.as_mut())
.expect("capability check already confirmed handle is open with a real file")
}
fn write_bytes_to_fd(&mut self, fd: u8, bytes: &[u8]) -> std::io::Result<()> {
match fd {
1 | 2 => self.host.write(fd, bytes),
_ => match self.file_handles.get_mut(&fd).and_then(|h| h.file.as_mut()) {
Some(file) => file.write_all(bytes),
None => Err(std::io::Error::other("file descriptor not open")),
},
}
}
fn write_string<const UNIT: usize>(&mut self, handle: u8, addr: u64) -> std::io::Result<u64> {
let mut chunk = Vec::new();
let mut cur = addr;
let mut count = 0u64;
loop {
chunk.clear();
let mut ended = false;
while chunk.len() < TRANSFER_CHUNK {
let character: [u8; UNIT] =
std::array::from_fn(|i| self.read_byte(cur.wrapping_add(i as u64)));
if character == [0; UNIT] {
ended = true;
break;
}
chunk.extend_from_slice(&character);
cur = cur.wrapping_add(UNIT as u64);
}
if !chunk.is_empty() || count == 0 {
self.write_bytes_to_fd(handle, &chunk)?;
}
count += (chunk.len() / UNIT) as u64;
if ended {
return Ok(count);
}
}
}
fn handle_fputs(&mut self, handle: u8) -> bool {
let str_addr = self.get_register(255);
debug!(
handle,
str_addr = format!("0x{:X}", str_addr),
"TRAP: Fputs"
);
if self.fail_unless(self.handle_writable(handle), -1) {
return true;
}
self.note_write(handle);
match self.write_string::<1>(handle, str_addr) {
Ok(count) => self.set_register(255, count),
Err(_) => {
debug!(handle, "Fputs write failed");
self.set_register(255, (-1i64) as u64);
}
}
self.advance_pc();
true
}
fn handle_fputc(&mut self, handle: u8) -> bool {
let ch = (self.get_register(255) & 0xFF) as u8;
debug!(handle, ch = format!("0x{:02X}", ch), "TRAP: Fputc");
if self.fail_unless(self.handle_writable(handle), -1) {
return true;
}
self.note_write(handle);
match self.write_bytes_to_fd(handle, &[ch]) {
Ok(_) => self.set_register(255, 0),
Err(_) => {
debug!(handle, "Fputc write failed");
self.set_register(255, (-1i64) as u64);
}
}
self.advance_pc();
true
}
fn handle_fputws(&mut self, handle: u8) -> bool {
let str_addr = self.get_register(255);
debug!(
handle,
str_addr = format!("0x{:X}", str_addr),
"TRAP: Fputws"
);
if self.fail_unless(self.handle_writable(handle), -1) {
return true;
}
self.note_write(handle);
match self.write_string::<2>(handle, str_addr) {
Ok(count) => self.set_register(255, count),
Err(_) => {
debug!(handle, "Fputws write failed");
self.set_register(255, (-1i64) as u64);
}
}
self.advance_pc();
true
}
fn handle_fseek(&mut self, handle: u8) -> bool {
let offset = self.get_register(255) as i64;
debug!(handle, offset, "TRAP: Fseek");
if self.fail_unless(self.handle_seekable(handle), -1) {
return true;
}
self.note_seek(handle);
let seek_from = if offset >= 0 {
SeekFrom::Start(offset as u64)
} else {
SeekFrom::End(offset + 1)
};
let file = self.open_file(handle);
match file.seek(seek_from) {
Ok(pos) => {
self.set_register(255, 0);
debug!(pos, "Seek successful");
}
Err(_) => {
self.set_register(255, (-1i64) as u64);
debug!("Seek failed");
}
}
self.advance_pc();
true
}
fn handle_ftell(&mut self, handle: u8) -> bool {
debug!(handle, "TRAP: Ftell");
if self.fail_unless(self.handle_seekable(handle), -1) {
return true;
}
let file = self.open_file(handle);
match file.stream_position() {
Ok(pos) => {
self.set_register(255, pos);
debug!(pos, "Ftell successful");
}
Err(_) => {
self.set_register(255, (-1i64) as u64);
debug!("Ftell failed");
}
}
self.advance_pc();
true
}
fn handle_debug(&mut self, index: u8) -> bool {
match self.debug_strings.get(index as usize) {
Some(text) => {
let mut bytes = text.clone();
bytes.push(b'\n');
if let Err(err) = self.host.write(1, &bytes) {
self.host
.diagnostic(&format!("debug: write to handle 1 failed: {err}"));
}
}
None => {
self.host
.diagnostic(&format!("debug: index {index} has no string in the table"));
}
}
self.advance_pc();
true
}
fn handle_time(&mut self, unit: u8) -> bool {
let micros = self.host.now_micros();
let time_value = match unit {
0 => micros / 1_000_000, 1 => micros / 1_000, 2 => micros, _ => {
debug!("Invalid time unit: {}", unit);
0
}
};
debug!("TRAP: Time (unit={}) => {}", unit, time_value);
self.set_register(255, time_value);
self.advance_pc();
true
}
}