mirror of
https://github.com/inexorabletash/jsbasic.git
synced 2024-11-30 01:49:29 +00:00
602 lines
17 KiB
JavaScript
602 lines
17 KiB
JavaScript
//
|
|
// DOS Emulation
|
|
|
|
// Usage:
|
|
// var dos = new DOS( tty ) // hooks tty's writeChar/readChar/readLine
|
|
// dos.reset() // Close all open buffers
|
|
|
|
function DOS(tty) {
|
|
|
|
var DOSErrors = {
|
|
LANGUAGE_NOT_AVAILABLE: [1, "Language not available"],
|
|
RANGE_ERROR: [2, 'Range error'],
|
|
WRITE_PROTECTED: [4, 'Write protected'],
|
|
END_OF_DATA: [5, 'End of data'],
|
|
FILE_NOT_FOUND: [6, 'File not found'],
|
|
VOLUME_MISMATCH: [7, 'Volume mismatch'],
|
|
IO_ERROR: [8, 'I/O error'],
|
|
DISK_FULL: [9, 'Disk full'],
|
|
FILE_LOCKED: [10, 'File locked'],
|
|
INVALID_OPTION: [11, 'Invalid option'],
|
|
NO_BUFFERS_AVAILABLE: [12, 'No buffers available'],
|
|
FILE_TYPE_MISMATCH: [13, 'File type mismatch'],
|
|
PROGRAM_TOO_LARGE: [14, 'Program too large'],
|
|
NOT_DIRECT_COMMAND: [15, 'Not direct command'],
|
|
|
|
// Re-used
|
|
SYNTAX_ERROR: [16, "Syntax error"]
|
|
},
|
|
|
|
STORAGE_PREFIX = 'vfs/',
|
|
|
|
// For MON/NOMON
|
|
MON_I = 1,
|
|
MON_C = 2,
|
|
MON_O = 4,
|
|
|
|
// Original versions of hooked I/O routines
|
|
tty_readLine,
|
|
tty_readChar,
|
|
tty_writeChar,
|
|
|
|
// Hooked I/O routines
|
|
hooked_readLine,
|
|
hooked_readChar,
|
|
hooked_writeChar,
|
|
|
|
// character output state
|
|
commandBuffer = "",
|
|
commandMode = false,
|
|
|
|
// I/O buffers
|
|
buffers = {},
|
|
activebuffer = null,
|
|
mode = "",
|
|
|
|
// other state
|
|
monico = 0;
|
|
|
|
function doserror(msg) {
|
|
throw new basic.RuntimeError(msg[1], msg[0]);
|
|
}
|
|
|
|
// Internal - crack arguments e.g. ",S6,D1"
|
|
function parseArgs(str, opts) {
|
|
str = str || '';
|
|
opts = opts || '';
|
|
|
|
// Set these to zero so they're always defined when passed into command handlers
|
|
var args = {
|
|
V: 0, // Volume
|
|
D: 0, // Drive
|
|
S: 0, // Slot
|
|
L: 0, // Length
|
|
R: 0, // Record/Relative
|
|
B: 0, // Byte
|
|
A: 0, // Address
|
|
C: undefined, // Echo Commands
|
|
I: undefined, // Echo Input
|
|
O: undefined // Echo Output
|
|
};
|
|
|
|
var m;
|
|
while ((m = str.match(/^,?\s*([VDSLRBACIO])\s*([0-9]+|\$[0-9A-Fa-f]+)?\s*([\x20-\x7E]*)/))) {
|
|
if (opts.indexOf(m[1]) === -1) {
|
|
doserror(DOSErrors.INVALID_OPTION);
|
|
}
|
|
args[m[1]] = Number(m[2]);
|
|
str = m[3];
|
|
}
|
|
|
|
if (str.length > 0) {
|
|
doserror(DOSErrors.INVALID_OPTION);
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// Browser-side VFS
|
|
//----------------------------------------------------------------------
|
|
|
|
function vfs_set(key, value) {
|
|
return window.localStorage.setItem(STORAGE_PREFIX + key, encodeURIComponent(value));
|
|
}
|
|
function vfs_get(key) {
|
|
var item = window.localStorage.getItem(STORAGE_PREFIX + key);
|
|
return item !== null ? decodeURIComponent(item) : null;
|
|
}
|
|
function vfs_remove(key) {
|
|
return window.localStorage.removeItem(STORAGE_PREFIX + key);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// Implementation
|
|
//----------------------------------------------------------------------
|
|
|
|
this.reset = function reset() {
|
|
buffers = {};
|
|
activebuffer = null;
|
|
mode = "";
|
|
};
|
|
|
|
function unlink(filename) {
|
|
var item = vfs_get(filename);
|
|
|
|
if (item === null) {
|
|
doserror(DOSErrors.FILE_NOT_FOUND);
|
|
}
|
|
|
|
vfs_remove(filename);
|
|
}
|
|
|
|
function rename(oldname, newname) {
|
|
var item = vfs_get(oldname);
|
|
|
|
if (item === null) {
|
|
doserror(DOSErrors.FILE_NOT_FOUND);
|
|
}
|
|
|
|
vfs_remove(oldname);
|
|
vfs_set(newname, item);
|
|
}
|
|
|
|
function open(filename, recordlength) {
|
|
if (recordlength === 0) {
|
|
// Sequential access
|
|
recordlength = 1;
|
|
}
|
|
|
|
// Peek in the VFS cache first
|
|
var file = vfs_get(filename),
|
|
req, url, async;
|
|
if (file === null) {
|
|
// Not cached - do a synchronous XmlHttpRequest for the file here
|
|
req = new XMLHttpRequest();
|
|
url = "vfs/" + encodeURIComponent(filename.replace(/\./g, '_')) + ".txt";
|
|
async = false;
|
|
req.open("GET", url, async);
|
|
req.send(null);
|
|
if (req.status === 200 || req.status === 0) { // 0 for file:// protocol
|
|
file = req.responseText.replace(/\r\n/g, "\r");
|
|
vfs_set(filename, file);
|
|
}
|
|
}
|
|
|
|
// Create a buffer for the file
|
|
buffers[filename] = {
|
|
file: file,
|
|
recordlength: recordlength,
|
|
recordnum: 0,
|
|
filepointer: 0
|
|
};
|
|
}
|
|
|
|
function append(filename, recordlength) {
|
|
// Normal open logic
|
|
open(filename, recordlength);
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(buffers, filename)) {
|
|
doserror(DOSErrors.FILE_NOT_FOUND);
|
|
}
|
|
|
|
var buf = buffers[filename];
|
|
|
|
// Then seek to the end of the file
|
|
buf.filepointer = buf.file.length;
|
|
buf.recordnum = Math.floor(buf.filepointer / buf.recordlength);
|
|
}
|
|
|
|
function close(filename) {
|
|
var buf, fn;
|
|
|
|
// If not specified, close all buffers
|
|
if (!filename) {
|
|
for (fn in buffers) {
|
|
if (Object.prototype.hasOwnProperty.call(buffers, fn)) {
|
|
close(fn);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
buf = buffers[filename];
|
|
if (buf) {
|
|
// flush changes to "disk"
|
|
vfs_set(filename, buf.file);
|
|
|
|
delete buffers[filename];
|
|
if (buf === activebuffer) {
|
|
activebuffer = null;
|
|
mode = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
function read(filename, recordnum, bytenum) {
|
|
var buf = buffers[filename];
|
|
if (!buf) {
|
|
// Open file if no such named buffer, but don't create it
|
|
open(filename, 0);
|
|
buf = buffers[filename];
|
|
}
|
|
|
|
if (buf.file === null) {
|
|
doserror(DOSErrors.FILE_NOT_FOUND);
|
|
}
|
|
|
|
// Set the file position
|
|
buf.recordnum = recordnum;
|
|
buf.filepointer = buf.recordlength * recordnum + bytenum;
|
|
|
|
// Set the active buffer into read mode
|
|
activebuffer = buf;
|
|
mode = "r";
|
|
}
|
|
|
|
function write(filename, recordnum, bytenum) {
|
|
var buf = buffers[filename];
|
|
if (!buf) {
|
|
// Must open the file before writing
|
|
doserror(DOSErrors.FILE_NOT_FOUND);
|
|
}
|
|
|
|
if (buf.file === null) {
|
|
// If we still don't have it, create in VFS if necessary
|
|
vfs_set(filename, '');
|
|
buf.file = '';
|
|
}
|
|
|
|
// Set up the file position
|
|
buf.recordnum = recordnum;
|
|
if (buf.recordlength > 1) {
|
|
buf.filepointer = buf.recordlength * recordnum;
|
|
}
|
|
buf.filepointer += bytenum;
|
|
|
|
// Set the active buffer into write mode
|
|
activebuffer = buf;
|
|
mode = "w";
|
|
}
|
|
|
|
function position(filename, records) {
|
|
var buf = buffers[filename];
|
|
if (!buf) {
|
|
// Open file if no such named buffer, but don't create it
|
|
open(filename, 0, false);
|
|
buf = buffers[filename];
|
|
}
|
|
|
|
// Set up the file position
|
|
buf.recordnum += records;
|
|
buf.filepointer += buf.recordlength * records;
|
|
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Command Dispatch
|
|
//----------------------------------------------------------------------
|
|
function executeCommand(command) {
|
|
// Delegate to various commands
|
|
// http://www.xs4all.nl/~fjkraan/comp/apple2faq/app2doscmdfaq.html
|
|
// http://www.textfiles.com/apple/ANATOMY/
|
|
|
|
var filename, filename2, args, slot;
|
|
|
|
if (monico & MON_C && tty) {
|
|
tty.writeString(command + "\r");
|
|
}
|
|
|
|
var m;
|
|
if ((m = command.match(/^MON([\x20-\x7E]*)/))) {
|
|
// MON[,C][,I][,O] Traces DOS 3.3 commands ('Commands', 'Input' and 'Output')
|
|
args = parseArgs(m[1], 'ICO');
|
|
|
|
if (args.I !== undefined) {
|
|
monico |= MON_I;
|
|
}
|
|
if (args.C !== undefined) {
|
|
monico |= MON_C;
|
|
}
|
|
if (args.O !== undefined) {
|
|
monico |= MON_O;
|
|
}
|
|
|
|
} else if ((m = command.match(/^NOMON([\x20-\x7E]*)/))) {
|
|
// NOMON[,C][,I][,O] Cancels tracing of DOS 3.3 commands ('Commands', 'Input' and 'Output')
|
|
args = parseArgs(m[1], 'ICO');
|
|
if (args.I !== undefined) {
|
|
monico &= ~MON_I;
|
|
}
|
|
if (args.C !== undefined) {
|
|
monico &= ~MON_C;
|
|
}
|
|
if (args.O !== undefined) {
|
|
monico &= ~MON_O;
|
|
}
|
|
} else if ((m = command.match(/^OPEN\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// OPEN filename[,Llen] Opens a text file.
|
|
filename = m[1];
|
|
args = parseArgs(m[2], 'L');
|
|
open(filename, args.L);
|
|
} else if ((m = command.match(/^APPEND\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// APPEND filename Appends to a text file.
|
|
filename = m[1];
|
|
args = parseArgs(m[2]);
|
|
append(filename, args.L);
|
|
} else if ((m = command.match(/^CLOSE\s*([\x20-\x2B\x2D-\x7E]+)?(,[\x20-\x7E]*)?/))) {
|
|
// CLOSE [filename] Closes specified (or all) open text files.
|
|
filename = m[1];
|
|
close(filename);
|
|
} else if ((m = command.match(/^POSITION\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// POSITION filename[,Rnum] Advances position in text file.
|
|
filename = m[1];
|
|
args = parseArgs(m[2], 'R');
|
|
position(filename, args.R);
|
|
} else if ((m = command.match(/^READ\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// READ filename[,Rnum][,Bbyte] Reads from a text file.
|
|
filename = m[1];
|
|
args = parseArgs(m[2], 'RB');
|
|
read(filename, args.R, args.B);
|
|
} else if ((m = command.match(/^WRITE\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// WRITE filename[,Rnum][,Bbyte] Writes to a text file.
|
|
filename = m[1];
|
|
args = parseArgs(m[2], 'RB');
|
|
write(filename, args.R, args.B);
|
|
} else if ((m = command.match(/^DELETE\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// DELETE filename Delete a file
|
|
filename = m[1];
|
|
args = parseArgs(m[2]);
|
|
unlink(filename);
|
|
} else if ((m = command.match(/^RENAME\s*([\x20-\x2B\x2D-\x7E]+),\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// RENAME filename,filename Rename a file
|
|
filename = m[1];
|
|
filename2 = m[2];
|
|
args = parseArgs(m[3]);
|
|
rename(filename, filename2);
|
|
} else if ((m = command.match(/^PR#\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// PR# slot Direct output to slot
|
|
slot = Number(m[1]);
|
|
args = parseArgs(m[2]);
|
|
if (slot === 0) {
|
|
if (tty.setFirmwareActive) { tty.setFirmwareActive(false); }
|
|
hooked_writeChar = tty_writeChar;
|
|
} else if (slot === 3) {
|
|
if (tty.setFirmwareActive) { tty.setFirmwareActive(true); }
|
|
hooked_writeChar = tty_writeChar;
|
|
} else if (slot === 4) {
|
|
hooked_writeChar = clock_writeChar;
|
|
} else {
|
|
doserror(DOSErrors.RANGE_ERROR);
|
|
}
|
|
} else if ((m = command.match(/^IN#\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/))) {
|
|
// IN# slot Direct input to slot
|
|
slot = Number(m[1]);
|
|
args = parseArgs(m[2]);
|
|
if (slot === 0 || slot === 3) {
|
|
hooked_readLine = tty_readLine;
|
|
hooked_readChar = tty_readChar;
|
|
} else if (slot === 4) {
|
|
hooked_readLine = clock_readLine;
|
|
hooked_readChar = clock_readChar;
|
|
} else {
|
|
doserror(DOSErrors.RANGE_ERROR);
|
|
}
|
|
} else if ((m = command.match(/^$/))) {
|
|
// Null command - terminates a READ/WRITE, but doesn't CLOSE
|
|
// (leaves record length intact on open buffer)
|
|
activebuffer = null;
|
|
mode = "";
|
|
} else {
|
|
doserror(DOSErrors.SYNTAX_ERROR);
|
|
}
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// Install TTY Hooks
|
|
//----------------------------------------------------------------------
|
|
tty_readLine = tty.readLine;
|
|
tty_readChar = tty.readChar;
|
|
tty_writeChar = tty.writeChar;
|
|
|
|
hooked_readLine = tty_readLine;
|
|
hooked_readChar = tty_readChar;
|
|
hooked_writeChar = tty_writeChar;
|
|
|
|
tty.readLine = dos_readLine;
|
|
tty.readChar = dos_readChar;
|
|
tty.writeChar = dos_writeChar;
|
|
|
|
function dos_readLine(callback, prompt) {
|
|
|
|
var string = "", c, data, len, fp, buffer;
|
|
if (mode === "r") {
|
|
// Cache for performance
|
|
data = activebuffer.file;
|
|
len = data.length;
|
|
fp = activebuffer.filepointer;
|
|
|
|
if (fp >= len) {
|
|
doserror(DOSErrors.END_OF_DATA);
|
|
}
|
|
|
|
buffer = [];
|
|
while (fp < len) {
|
|
// Sequential Access
|
|
c = data[fp];
|
|
fp += 1;
|
|
if (c === "\r" || c === "\n" || c === "\x00") {
|
|
break;
|
|
} else {
|
|
buffer.push(c);
|
|
}
|
|
}
|
|
activebuffer.filepointer = fp;
|
|
string = buffer.join("");
|
|
|
|
if (monico & MON_I) {
|
|
tty.writeString(prompt + string + "\r");
|
|
}
|
|
|
|
// Non-blocking return
|
|
setTimeout(function() { callback(string); }, 0);
|
|
} else {
|
|
hooked_readLine(callback, prompt);
|
|
}
|
|
}
|
|
|
|
function dos_readChar(callback) {
|
|
|
|
var character = "";
|
|
if (mode === "r") {
|
|
if (activebuffer.filepointer >= activebuffer.file.length) {
|
|
doserror(DOSErrors.END_OF_DATA);
|
|
}
|
|
|
|
character = activebuffer.file[activebuffer.filepointer];
|
|
activebuffer.filepointer += 1;
|
|
|
|
if (monico & MON_I && tty) {
|
|
hooked_writeChar(character);
|
|
}
|
|
|
|
// Non-blocking return
|
|
setTimeout(function() { callback(character); }, 0);
|
|
} else {
|
|
hooked_readChar(callback);
|
|
}
|
|
}
|
|
|
|
function dos_writeChar(c) {
|
|
|
|
if (commandMode) {
|
|
if (c === "\r") {
|
|
commandMode = false;
|
|
executeCommand(commandBuffer);
|
|
commandBuffer = "";
|
|
} else {
|
|
commandBuffer += c;
|
|
}
|
|
return;
|
|
} else if (c === "\x04") {
|
|
commandBuffer = "";
|
|
commandMode = true;
|
|
return;
|
|
}
|
|
|
|
if (mode === "w") {
|
|
var buf, d;
|
|
|
|
if (monico & MON_O) {
|
|
hooked_writeChar(c);
|
|
}
|
|
|
|
buf = activebuffer;
|
|
// Extend file to necessary length
|
|
while (buf.filepointer > buf.file.length) {
|
|
buf.file += "\x00";
|
|
}
|
|
|
|
// Append or insert character
|
|
if (buf.filepointer === buf.file.length) {
|
|
buf.file += c;
|
|
} else {
|
|
d = buf.file.substring(0, buf.filepointer);
|
|
d += c;
|
|
d += buf.file.substring(buf.filepointer + 1);
|
|
buf.file = d;
|
|
}
|
|
|
|
buf.filepointer += 1;
|
|
} else {
|
|
hooked_writeChar(c);
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Clock routine - vaguely ThunderClock compatible
|
|
//----------------------------------------------------------------------
|
|
|
|
var clockbuf = '';
|
|
function clock_writeChar(c) {
|
|
var DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
|
var MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
|
|
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
|
|
function spad2(s) {
|
|
return ('00' + String(s)).slice(-2);
|
|
}
|
|
function zpad2(s) {
|
|
return ('00' + String(s)).slice(-2);
|
|
}
|
|
function zpad3(s) {
|
|
return ('000' + String(s)).slice(-3);
|
|
}
|
|
|
|
var now = new Date();
|
|
switch (c) {
|
|
default:
|
|
case '%': // AM/PM ASCII string mode - e.g. "TUE MAY 12 4:32:55 PM"
|
|
case '>': // AM/PM ASCII string mode - e.g. "TUE MAY 12 4:32:55 PM"
|
|
clockbuf =
|
|
DAYS[now.getDay()] + ' ' +
|
|
MONTHS[now.getMonth()] + ' ' +
|
|
now.getDate() + ' ' +
|
|
spad2((now.getHours() === 0 ? 12 : now.getHours() > 12 ? now.getHours() - 12 : now.getHours())) + ':' +
|
|
zpad2(now.getMinutes()) + ':' +
|
|
zpad2(now.getSeconds()) + ' ' +
|
|
(now.getHours() < 12 ? 'AM' : 'PM');
|
|
break;
|
|
case '&': // 24 hour ASCII string - e.g. "TUE MAY 12 16:32:55"
|
|
case '<': // 24 hour ASCII string - e.g. "TUE MAY 12 16:32:55"
|
|
clockbuf =
|
|
DAYS[now.getDay()] + ' ' +
|
|
MONTHS[now.getMonth()] + ' ' +
|
|
now.getDate() + ' ' +
|
|
spad2(now.getHours()) + ':' +
|
|
zpad2(now.getMinutes()) + ':' +
|
|
zpad2(now.getSeconds()) + ' ' +
|
|
(now.getHours() < 12 ? 'AM' : 'PM');
|
|
break;
|
|
case ' ': // Mountain Computer Apple Clock Format - e.g. "05/12 16;32;55.000"
|
|
clockbuf =
|
|
zpad2(now.getMonth()+1) + '/' +
|
|
zpad2(now.getDate()) + ' ' +
|
|
zpad2(now.getHours()) + ';' +
|
|
zpad2(now.getMinutes()) + ';' +
|
|
zpad2(now.getSeconds()) + '.' +
|
|
zpad3(now.getMilliseconds());
|
|
break;
|
|
case '#': // Numeric format, e.g. MO,DW,DT,HR,MN,SEC
|
|
clockbuf = [
|
|
now.getMonth()+1,
|
|
now.getDay(),
|
|
now.getDate(),
|
|
now.getHours(),
|
|
now.getMinutes(),
|
|
now.getSeconds()
|
|
].join(',');
|
|
break;
|
|
}
|
|
clockbuf += '\r';
|
|
}
|
|
function clock_readLine(callback, prompt) {
|
|
tty.writeString(prompt); // TODO: Correct? Newline?
|
|
var tmp = clockbuf;
|
|
clockbuf = '';
|
|
setTimeout(function() { callback(tmp); }, 0);
|
|
}
|
|
function clock_readChar(callback) {
|
|
if (!clockbuf.length) {
|
|
setTimeout(function() { callback('\r'); }, 0);
|
|
} else {
|
|
var c = clockbuf.substring(0, 1);
|
|
clockbuf = clockbuf.slice(1);
|
|
setTimeout(function() { callback(c); }, 0);
|
|
}
|
|
}
|
|
}
|