jsbasic/dos.js

510 lines
14 KiB
JavaScript

//
// Applesoft BASIC in Javascript
// DOS Emulation
// Copyright (C) 2009-2011 Joshua Bell
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Usage:
// var dos = new DOS( tty ) // hooks tty's writeChar/readChar/readLine
// dos.reset() // Close all open buffers
/*global basic*/
function DOS(tty) {
/*jslint bitwise: false, browser: true*/
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']
},
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,
// 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) {
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
};
while (str.match(/^,?\s*([VDSLRBACIO])\s*([0-9]+|\$[0-9A-Fa-f]+)?\s*([\x20-\x7E]*)/)) {
if (opts.indexOf(RegExp.$1) === -1) {
doserror(DOSErrors.INVALID_OPTION);
}
args[RegExp.$1] = Number(RegExp.$2);
str = RegExp.$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();
try {
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);
}
} catch (e) {
// File doesn't exist - APPEND/READ will fail
throw e;
}
}
// 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 (!buffers.hasOwnProperty(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 (buffers.hasOwnProperty(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");
}
if (command.match(/^MON([\x20-\x7E]*)/)) {
// MON[,C][,I][,O] Traces DOS 3.3 commands ('Commands', 'Input' and 'Output')
args = parseArgs(RegExp.$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 (command.match(/^NOMON([\x20-\x7E]*)/)) {
// NOMON[,C][,I][,O] Cancels tracing of DOS 3.3 commands ('Commands', 'Input' and 'Output')
args = parseArgs(RegExp.$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 (command.match(/^OPEN\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// OPEN filename[,Llen] Opens a text file.
filename = RegExp.$1;
args = parseArgs(RegExp.$2, 'L');
open(filename, args.L);
} else if (command.match(/^APPEND\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// APPEND filename Appends to a text file.
filename = RegExp.$1;
args = parseArgs(RegExp.$2);
append(filename, args.L);
} else if (command.match(/^CLOSE\s*([\x20-\x2B\x2D-\x7E]+)?(,[\x20-\x7E]*)?/)) {
// CLOSE [filename] Closes specified (or all) open text files.
filename = RegExp.$1;
close(filename);
} else if (command.match(/^POSITION\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// POSITION filename[,Rnum] Advances position in text file.
filename = RegExp.$1;
args = parseArgs(RegExp.$2, 'R');
position(filename, args.R);
} else if (command.match(/^READ\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// READ filename[,Rnum][,Bbyte] Reads from a text file.
filename = RegExp.$1;
args = parseArgs(RegExp.$2, 'RB');
read(filename, args.R, args.B);
} else if (command.match(/^WRITE\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// WRITE filename[,Rnum][,Bbyte] Writes to a text file.
filename = RegExp.$1;
args = parseArgs(RegExp.$2, 'RB');
write(filename, args.R, args.B);
} else if (command.match(/^DELETE\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// DELETE filename Delete a file
filename = RegExp.$1;
args = parseArgs(RegExp.$2);
unlink(filename);
} else if (command.match(/^RENAME\s*([\x20-\x2B\x2D-\x7E]+),\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// RENAME filename,filename Rename a file
filename = RegExp.$1;
filename2 = RegExp.$2;
args = parseArgs(RegExp.$3);
rename(filename, filename2);
} else if (command.match(/^PR#\s*([\x20-\x2B\x2D-\x7E]+)(,[\x20-\x7E]*)?/)) {
// PR# slot Direct output to slot
slot = Number(RegExp.$1);
args = parseArgs(RegExp.$2);
if (slot === 0) {
if (tty.setFirmwareActive) { tty.setFirmwareActive(false); }
} else if (slot === 3) {
if (tty.setFirmwareActive) { tty.setFirmwareActive(true); }
} else {
doserror(DOSErrors.RANGE_ERROR);
}
} else if (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.INVALID_OPTION);
}
}
//----------------------------------------------------------------------
// Install TTY Hooks
//----------------------------------------------------------------------
tty_readLine = tty.readLine;
tty_readChar = tty.readChar;
tty_writeChar = tty.writeChar;
tty.readLine = 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 {
tty_readLine(callback, prompt);
}
};
tty.readChar = 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) {
tty_writeChar(character);
}
// Non-blocking return
setTimeout(function() { callback(character); }, 0);
} else {
tty_readChar(callback);
}
};
tty.writeChar = 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) {
tty_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 {
tty_writeChar(c);
}
}; // writeChar
}