webamp/experiments/maki-interpreter/parser.js
2019-07-05 07:47:10 -07:00

394 lines
9.8 KiB
JavaScript

const { COMMANDS } = require("./constants");
const Variable = require("./variable");
const MAGIC = "FG";
const ENCODING = "binary";
const PRIMITIVE_TYPES = {
5: "BOOLEAN",
2: "INT",
3: "FLOAT",
4: "DOUBLE",
6: "STRING"
};
// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
// file. When we want to run in the browser, we can refactor this class to use a
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
class MakiFile {
constructor(buffer) {
this._arr = new Uint8Array(buffer);
this._buffer = buffer;
this._i = 0;
}
readUInt32LE() {
const int = this.peekUInt32LE();
this._i += 4;
return int;
}
peekUInt32LE() {
const int = this._buffer.readUInt32LE(this._i);
const arrInt = this.__readUInt32LE__arr();
if (int !== arrInt) {
throw new Error("Array value does not equal buffer value");
}
return int;
}
__readUInt32LE__arr() {
const offset = this._i >>> 0;
return (
(this._arr[offset] |
(this._arr[offset + 1] << 8) |
(this._arr[offset + 2] << 16)) +
this._arr[offset + 3] * 0x1000000
);
}
readUInt16LE() {
const int = this._buffer.readUInt16LE(this._i);
const arrInt = this.__readUInt16LE__arr();
if (int !== arrInt) {
throw new Error("Array value does not equal buffer value");
}
this._i += 2;
return int;
}
__readUInt16LE__arr() {
const offset = this._i >>> 0;
return this._arr[offset] | (this._arr[offset + 1] << 8);
}
readUInt8() {
const int = this._buffer.readUInt8(this._i);
const arrInt = this._arr[this._i];
if (int !== arrInt) {
throw new Error("Array value does not equal buffer value");
}
this._i++;
return int;
}
readStringOfLength(length) {
const str = this._buffer.toString(ENCODING, this._i, this._i + length);
const arrStr = this.__readString__arr(length);
if (str !== arrStr) {
throw new Error("Array value does not equal buffer value");
}
this._i += length;
return str;
}
readString() {
return this.readStringOfLength(this.readUInt16LE());
}
getPosition() {
return this._i;
}
__readString__arr(length) {
let ret = "";
const end = Math.min(this._arr.length, this._i + length);
for (var i = this._i; i < end; ++i) {
ret += String.fromCharCode(this._arr[i]);
}
return ret;
}
}
function readMagic(makiFile) {
const magic = makiFile.readStringOfLength(MAGIC.length);
if (magic !== MAGIC) {
throw new Error("Magic number does not mach. Is this a maki file?");
}
return magic;
}
function readVersion(makiFile) {
// No idea what we're actually expecting here.
return makiFile.readUInt16LE();
}
function readClasses(makiFile) {
let count = makiFile.readUInt32LE();
const classes = [];
while (count--) {
let identifier = "";
let chunks = 4;
while (chunks--) {
identifier += makiFile
.readUInt32LE()
.toString(16)
.padStart(8, "0");
}
classes.push(identifier);
}
return classes;
}
function readMethods(makiFile) {
let count = makiFile.readUInt32LE();
const methods = [];
while (count--) {
const classCode = makiFile.readUInt16LE();
// Offset into our parsed types
const typeOffset = classCode & 0xff;
// This is probably the second half of a uint32
makiFile.readUInt16LE();
const name = makiFile.readString();
methods.push({ name, typeOffset });
}
return methods;
}
function readVariables({ makiFile, classes }) {
let count = makiFile.readUInt32LE();
const variables = [];
while (count--) {
const typeOffset = makiFile.readUInt8();
const object = makiFile.readUInt8();
const subClass = makiFile.readUInt16LE();
const uinit1 = makiFile.readUInt16LE();
const uinit2 = makiFile.readUInt16LE();
const uinit3 = makiFile.readUInt16LE();
const uinit4 = makiFile.readUInt16LE();
const global = makiFile.readUInt8();
const system = makiFile.readUInt8();
if (object) {
const klass = classes[typeOffset];
if (klass == null) {
throw new Error("Invalid type");
}
variables.push(new Variable({ type: klass, typeName: "OBJECT" }));
} else if (subClass) {
const variable = variables[typeOffset];
if (variable == null) {
throw new Error("Invalid type");
}
variables.push(new Variable({ type: variable, typeName: "SUBCLASS" }));
} else {
const typeName = PRIMITIVE_TYPES[typeOffset];
if (typeName == null) {
throw new Error("Invalid type");
}
let value = null;
switch (typeName) {
// BOOLEAN
case PRIMITIVE_TYPES[5]:
value = uinit1;
break;
// INT
case PRIMITIVE_TYPES[2]:
value = uinit1;
break;
case PRIMITIVE_TYPES[3]:
case PRIMITIVE_TYPES[4]:
const exponent = (uinit2 & 0xff80) >> 7;
const mantisse = ((0x80 | (uinit2 & 0x7f)) << 16) | uinit1;
value = mantisse * 2.0 ** (exponent - 0x96);
break;
case PRIMITIVE_TYPES[6]:
// This will likely get set by constants later on.
break;
default:
throw new Error("Invalid primitive type");
}
const variable = new Variable({ type: typeName, typeName });
variable.setValue(value);
variables.push(variable);
}
}
return variables;
}
function readConstants({ makiFile, variables }) {
let count = makiFile.readUInt32LE();
while (count--) {
const i = makiFile.readUInt32LE();
const variable = variables[i];
const value = makiFile.readString();
// TODO: Don't mutate
variable.setValue(value);
}
}
function readBindings(makiFile) {
let count = makiFile.readUInt32LE();
const bindings = [];
while (count--) {
const variableOffset = makiFile.readUInt32LE();
const methodOffset = makiFile.readUInt32LE();
const binaryOffset = makiFile.readUInt32LE();
bindings.push({ variableOffset, binaryOffset, methodOffset });
}
return bindings;
}
function decodeCode({ makiFile, classes, variables, methods, bindings }) {
const length = makiFile.readUInt32LE();
const start = makiFile.getPosition();
function getPos() {
return makiFile.getPosition() - start;
}
const localFunctions = {};
const results = [];
while (makiFile.getPosition() < start + length) {
const command = parseComand({
makiFile,
length,
pos: getPos(),
classes,
variables,
methods,
localFunctions
});
results.push(command);
}
// TODO: Don't mutate
Object.values(localFunctions).forEach(localFunction => {
bindings.push(localFunction);
});
bindings.sort((a, b) => {
return a.binaryOffset - b.binaryOffset;
});
return results;
}
// TODO: Refactor this to consume bytes directly off the end of MakiFile
function parseComand({ makiFile, length, pos, localFunctions }) {
const command = {};
const opcode = makiFile.readUInt8();
command.offset = pos;
command.pos = pos;
command.opcode = opcode;
command.arguments = [];
command.command = COMMANDS[opcode];
if (command.command == null) {
throw new Error(`Unknown opcode "${opcode}"`);
}
if (command.command.arg == null) {
return command;
}
const argValue = makiFile.readUInt32LE();
const argType = command.command.arg;
let arg = null;
switch (argType) {
case "var": {
arg = argValue;
break;
}
case "line": {
arg = argValue + 5;
break;
}
case "objFunc": {
// TODO: ClassesOffset
arg = argValue;
break;
}
case "func": {
// Note in the perl code here: "todo, something strange going on here..."
const variable = argValue + 5;
const offset = variable + pos;
arg = {
name: `func${offset}`,
code: [],
offset
};
if (localFunctions[offset] == null) {
localFunctions[offset] = {
function: arg,
offset
};
}
break;
}
case "obj": {
// Classes Offset
arg = argValue;
break;
}
default:
throw new Error("Invalid argType");
}
command.arguments = [arg];
// From perl: look forward for a stack protection block
// (why do I have to look FORWARD. stupid nullsoft)
if (
// Is there another UInt32 to read?
length > pos + 5 + 4 &&
makiFile.peekUInt32LE() >= 0xffff0000
) {
makiFile.readUInt32LE();
}
if (opcode === 112) {
makiFile.readUInt8();
}
return command;
}
function parse(buffer) {
const makiFile = new MakiFile(buffer);
const magic = readMagic(makiFile);
readVersion(makiFile);
makiFile.readUInt32LE(); // Not sure what we are skipping over here. Just some UInt 32.
const classes = readClasses(makiFile);
const methods = readMethods(makiFile);
const variables = readVariables({ makiFile: makiFile, classes });
readConstants({ makiFile: makiFile, variables });
const bindings = readBindings(makiFile);
const commands = decodeCode({
makiFile,
classes,
variables,
methods,
bindings
});
// Map binary offsets to command indexes.
// Some bindings/functions ask us to jump to a place in the binary data and
// start executing. However, we want to do all the parsing up front, and just
// return a list of commands. This map allows anything that mentions a binary
// offset to find the command they should jump to.
const offsetToCommand = {};
commands.forEach((command, i) => {
if (command.offset != null) {
offsetToCommand[command.offset] = i;
}
});
const resolvedBindings = bindings.map(binding => {
const { binaryOffset, ...rest } = binding;
return {
commandOffset: offsetToCommand[binaryOffset],
...rest
};
});
return {
magic,
classes,
methods,
variables,
bindings: resolvedBindings,
commands
};
}
module.exports = parse;