adder-script
Version:
Python like language to execute untrusted codes in browsers and Node.js.
585 lines (482 loc) • 19 kB
JavaScript
"use strict";
/**
* The class that loads compiled code and executes it.
*
* Author: Ronen Ness.
* Since: 2016
*/
// include errors
var Errors = require("./../errors");
// include jsface for classes
var jsface = require("./../dependencies/jsface"),
Class = jsface.Class,
extend = jsface.extend;
// include the alternative console
var Console = require("./../console");
// include default flags
var defaultFlags = require("./default_flags");
// include compiler
var Compiler = require('./../compiler');
// include language components
var Language = require("./../language");
// include core stuff
var Core = require("./../core");
// misc utils
var Utils = require("./../utils");
// create constant for default null value
var nullConst = new Core.Variable(this._context, null);
// the Interpreter class - load compiled code and executes it.
var Interpreter = Class({
// Interpreter constructor
// @param flags - Interpreter flags to set. for more info and defaults, see file default_flags.Js.
constructor: function(flags)
{
// store interpreter flags
this._flags = flags || {};
// set default flags
for (var key in defaultFlags)
{
if (this._flags[key] === undefined)
{
this._flags[key] = defaultFlags[key];
}
}
// show basic info
Console.info("Created new interpreter!");
Console.log("Interpreter flags", this._flags);
// currently not executing code
this._resetCurrExecutionData();
// set root block to null
this._rootBlock = null;
// create context
this._context = new Core.Context(this,
this._flags.stackLimit,
this._flags.maxVarsInScope);
// get built-in stuff
this._builtins = Language.Builtins;
// set all builtin functions
for (var key in this._builtins.Functions)
{
// if in removed builtin list skip
if (this._flags.removeBuiltins.indexOf(key) !== -1) {
continue;
}
// add builtin function
var val = this._builtins.Functions[key];
this._addBuiltinVar(key, val);
}
// some special consts - true, false, null, etc.
for (key in Language.Defs.keywords)
{
// if in removed builtin list skip
if (this._flags.removeBuiltins.indexOf(key) !== -1) {
continue;
}
// add builtin keyword
this._addBuiltinVar(Language.Defs.keywords[key], key);
}
// set all builtin consts
for (var key in this._builtins.Consts)
{
// if in removed builtin list skip
if (this._flags.removeBuiltins.indexOf(key) !== -1) {
continue;
}
// add builtin const
this._addBuiltinVar(key, this._builtins.Consts[key]);
}
// copy all the available statement types with their the corresponding keywords
this._statementTypes = {};
for (var key in Language.Statements)
{
var currStatement = Language.Statements[key];
this._statementTypes[currStatement.getKeyword()] = currStatement;
}
// show all builtins
Console.log("Interpreter builtins:", this._context.getAllIdentifiers());
},
// reset the data of the current execution
_resetCurrExecutionData: function() {
this._currExecution = {
isDone: false,
error: null,
lastVal: nullConst,
currStatement: null,
currLine: -1,
estimatedMemoryUsage: 0,
maxStatementsPerRun: this._flags.maxStatementsPerRun,
executionTimeLimit: this._flags.executionTimeLimit,
executionStartTime: this._getCurrTimeMs(),
}
},
// set builtin var
_addBuiltinVar: function(key, val)
{
var readonly = true;
var force = true;
var variable = new Core.Variable(this._context, val);
this._context.setVar(key, variable, readonly, force);
},
// include a module and export it to the scripts.
// this is the most basic way to include / exclude functionality by specific modules.
// @param moduleId - string, the module name.
// @param module - (optional) module type, use it if you want to add module that is not a built-in module.
addModule: function(moduleId, module)
{
// special case - if moduleId is "ALL" or "SAFE", add all modules
if (moduleId === "ALL" || moduleId === "SAFE")
{
// iterate over all built-in modules
for (var key in Language.Modules) {
// if requested only safe modules make sure its a production-safe module
if (moduleId === "SAFE" && !(new Language.Modules[key]()).isSafe) {
continue;
}
// add module
this.addModule(key);
}
return;
}
Console.log("Load module: ", moduleId);
// get module and make sure exist
if (module === undefined)
{
var module = Language.Modules[moduleId];
if (module === undefined) {throw new Errors.InterpreterError("Module '" + moduleId + "' not defined.");}
}
// add module to builtin vars
this._addBuiltinVar(moduleId, new module(this._context, "Module." + moduleId));
},
// define a new function
// @param name - function name.
// @param block - function block.
// @param arguments - list with function arguments names.
defineFunction: function(name, block, args)
{
// make sure we got a valid block
if (!block) {
throw new Errors.SyntaxError("Missing block for function '" + name + "'!");
}
// create variable for this function
var newVar = new Core.Variable(this._context, {
isFunction: true,
name: name,
block: block,
arguments: args,
requiredArgs: args.length,
toString: function() {
return '<' + this.name + '>';
},
toRepr: function() {
return this.toString();
},
});
// add function to current scope
this._context.setVar(name, newVar);
return newVar;
},
// call a function.
// @param func - function object or the variable containing it.
// @param args - args to call with.
// @param object - optional, object containing the function to call.
// @return function ret value.
callFunction: function(func, args, object)
{
// if function is string, get it from context
if (typeof func === "string")
func = this._context.getVar(func);
// if got a var containing the function, take the function value from it
if (func._value) {
func = func._value;
}
// make sure its a function
if (!func || !func.isFunction)
{
throw new Errors.RuntimeError("'" + func + "' is not a function!");
}
// validate number of args required
if (func.requiredArgs !== null &&
(args.length < func.requiredArgs || args.length > func.requiredArgs + func.optionalArgs))
{
throw new Errors.RuntimeError("'" + func + "' expect " + func.requiredArgs + " arguments. " + args.length + " given.");
}
// is it a built-in function? call it
if (func.isBuiltinFunc)
{
// if this builtin function require native javascript params
if (func.convertParamsToNativeJs) {
if (!args.map) {args = [args];}
args = args.map(function(x) {
return (x && x.toNativeJs) ? x.toNativeJs() : x;
});
}
// else, convert args to Adder objects.
else
{
// normalize args
args = Core.Variable.makeAdderObjects(this._context, args)._value._list;
}
// make sure object is an Adder object
if (object !== undefined && !object.__isAdderObject) {
object = Core.Variable.makeAdderObjects(this._context, object)._value;
}
// call and return the function result
var ret = func.__imp.apply(object !== undefined ? object : this, args);
this._context.getScope().returnValue = ret;
this.setLastValue(ret);
return ret;
}
// if got here it means its a user-defined function
// create a new scope
this._context.stackPush("function");
// set arguments as local vars in function's scope
for (var i = 0; i < args.length; ++i)
{
this._context.setVar(func.arguments[i], args[i]);
}
// execute function's block
func.block.execute();
// when done pop stack and return value
var ret = this._context.getScope().returnValue;
this.setLastValue(ret);
this._context.stackPop();
return ret;
},
// return from current scope with a given value
returnValue: function(val)
{
// set return value register
var scope = this._context.getScope();
scope.returnValue = val;
scope.calledReturn = true;
},
// get current time in ms
_getCurrTimeMs: function()
{
return Utils.getTime();
},
// return execution time of current program
_getExecutionTime: function()
{
return this._getCurrTimeMs() - this._currExecution.executionStartTime;
},
// notify that current execution is done
// @param error - optional exception is occured
finishExecute: function(error)
{
// set done and last exception
this._currExecution.isDone = true;
this._currExecution.error = error;
// if we had an error clear stack
if (error) {
this._context.clearStack();
}
// if set to throw errors outside
if (error && this._flags.throwErrors) {
throw error;
}
},
// reset context (clear stack and remove all user-defined global vars)
resetContext: function() {
this._context.reset();
},
// re-throw execution exception, only if had one
propagateExecutionErrors: function()
{
if (this._currExecution.error)
{
throw this._currExecution.error;
}
},
// get last exception, if happened
getLastError: function()
{
return this._currExecution.error;
},
// update current execution memory usage
updateMemoryUsage: function(diff)
{
this._currExecution.estimatedMemoryUsage += diff;
if (this._flags.memoryAllocationLimit &&
this._currExecution.estimatedMemoryUsage > this._flags.memoryAllocationLimit) {
throw new Errors.ExceedMemoryLimit("Exceeded memory usage limit!");
}
},
// execute the currently loaded code
// @param funcName - if provided, will call this function instead of root block.
execute: function(funcName)
{
// no root block is set? exception
if (this._rootBlock === null) {
throw new Errors.InterpreterError("Tried to execute code without loading any code first!");
}
// reset execution data
this._resetCurrExecutionData();
// start execution
try {
if (funcName) {
this.callFunction(funcName, []);
}
else {
this._rootBlock.execute();
}
this.finishExecute();
}
catch(e) {
this.finishExecute(e);
}
},
// get last evaluate value
getLastValue: function()
{
return this._currExecution.lastVal;
},
// get last statement
getLastStatement: function()
{
return this._currExecution.currStatement;
},
// set the last evaluated value
setLastValue: function(value)
{
// special case for optimizations
if (value === undefined || value === null || value._value === null) {
this._currExecution.lastVal = nullConst;
return;
}
// make sure value is a variable
value = Core.Variable.makeVariables(this._context, value);
// set it
this._currExecution.lastVal = value;
},
// evaluate a statement
evalStatement: function(statement)
{
// reset last value
this.setLastValue(nullConst);
// set current line of code (for errors etc)
this._currExecution.currLine = statement._line;
this._currExecution.currStatement = statement;
// check statements limit
if (this._currExecution.maxStatementsPerRun !== null &&
this._currExecution.maxStatementsPerRun-- <= 0) {
throw new Errors.ExceededStatementsLimit(this._flags.maxStatementsPerRun);
}
// check for time limit
if (this._currExecution.executionTimeLimit !== null) {
if (this._getExecutionTime() > this._currExecution.executionTimeLimit) {
throw new Errors.ExceededTimeLimit(this._flags.executionTimeLimit);
}
}
try {
// execute command
var ret = statement.execute();
this.setLastValue(ret);
} catch (e) {
// add error line number
if (e.message && this._currExecution.currLine && e.line === undefined)
{
e.message += " [at line: " + this._currExecution.currLine + "]"
e.line = true;
}
throw e;
}
},
// evaluate a single code line
eval: function(code)
{
var compiler = new Compiler(this._flags);
var compiled = compiler.compile(code);
this.load(compiled);
this.execute();
},
// convert current ast line into a statement
__parseStatement: function(ast, line)
{
// check if first token is a predefined statement (like if, print, etc..)
var statementType = this._statementTypes[ast[0].value];
// if statement is not defined, parse this statement as a general expression (default)
statementType = statementType || this._statementTypes[""];
// instantiate statement
var currStatement = new statementType(this._context, ast, line);
return currStatement;
},
// load compiled code.
// @param compiledCode - the output of the compiler, basically a list of AST nodes.
load: function(compiledCode)
{
Console.debug("Loading code..", compiledCode);
// get context
var context = this._context;
// create global block and blocks queue
var globalBlock = new Core.Block(context);
var blocksQueue = [globalBlock];
// current block and last statement parsed
var currBlock = globalBlock;
var lastStatement = {openNewBlock: false};
// iterate over compiled code and parse statements
for (var i = 0; i < compiledCode.length; ++i)
{
// get current AST and line index
var currAst = compiledCode[i][0];
var line = compiledCode[i][1];
// did we just opened a new block
var openedNewBlock = false;
// special case - if new block
if (currAst === "NEW_BLOCK") {
// create new block and add it to blocks queue
var newBlock = new Core.Block(context);
currBlock.addBlock(newBlock);
blocksQueue.push(newBlock);
// mark that we just opened a new block in this statement
openedNewBlock = true;
continue;
}
// special case - if block ends
if (currAst === "END_BLOCK") {
// remove block from blocks list
blocksQueue.pop();
if (blocksQueue.length === 0) {
throw new Errors.InternalError("Invalid END_BLOCK token, ran out of blocks!");
}
continue;
}
// if last statement was a statement that opens a new block, make sure it did
if (lastStatement.OpenNewBlock && !openedNewBlock)
{
throw new Errors.SyntaxError('Missing block indent.', line);
}
// now check the opposite - opened a new block without proper reason
else if (!lastStatement.OpenNewBlock && openedNewBlock)
{
throw new Errors.SyntaxError('Unexpected block indent.', line);
}
// skip empty
if (currAst.length === 0) {continue;}
// parse into statement
var statement = this.__parseStatement(currAst, line);
// get current block
currBlock = blocksQueue[blocksQueue.length-1];
// get last statement in current block and set it as current statement previous statement
var lastStatementInBlock = currBlock._lastStatement;
statement.setPreviousStatement(lastStatementInBlock);
// add current statement
currBlock.addStatement(statement);
// store current statement as last statement
lastStatement = statement;
}
// set global block as our new root block
this._rootBlock = globalBlock;
Console.debug("Code loaded successfully!");
},
// return a debug representation of the blocks
getDebugBlocksView: function() {
return this._rootBlock.getDebugBlocksView(0);
},
// program output function
output: function() {
console.log.apply(null, arguments);
},
});
// export the Interpreter class
module.exports = Interpreter;