ejs icon indicating copy to clipboard operation
ejs copied to clipboard

Add custom function constructor option

Open ncake opened this issue 2 years ago • 3 comments

This adds an option called functionClass, which purpose is to allow to bring your own execution runtime.

Here's a basic example of how one could integrate quickjs-emscripten module to execute code in a WASM-powered virtual machine.

// index.js
import LoadSafeEJS from "./safe-ejs.js";

const safeEJS = await LoadSafeEJS();
const res = safeEJS.render("<%- 'hello ' + world %>", {world: 'world'});
console.log(res);
safeEJS.dispose();
// safe-ejs.js
import ejs from 'ejs/ejs.js';
import { getQuickJS } from "quickjs-emscripten"

/** @param {import('quickjs-emscripten/dist').QuickJSWASMModule} QuickJS */
function SafeEJS(QuickJS){
	const vm = QuickJS.newContext();

	this.dispose = function(){
		vm.dispose();
	};

	this.render = function(source, data, ejsOptions){
		return ejs.render(source, data, {...ejsOptions, functionClass})
	};

	const functionClass = function(argNames, funcBody){
		// not implemented here:
		// - caching the function between calls (thus useless for ejs.compile)
		// - async, include(), probably more
		return (locals, escapeFn, include, rethrow) => {
			// create function from source
			const ctor = vm.getProp(vm.global, 'Function');
			const argNamesHandle = vm.newString(argNames);
			const funcBodyHandle = vm.newString(funcBody);
			const newFuncRet = vm.callFunction(ctor, vm.undefined, argNamesHandle, funcBodyHandle);
			ctor.dispose();
			argNamesHandle.dispose();
			funcBodyHandle.dispose();
			const newFunc = vm.unwrapResult(newFuncRet);
			// wrap user-passed data into quickjs values
			const handleList = [];
			const makeDisposable = handle => (handleList.unshift(handle), handle);
			const localsHandle = wrapValue(locals, makeDisposable);
			// wrap the rest of the arguments
			const escapeFnHandle = vm.newFunction('escapeFn', strHandle => {
				const str = vm.getString(strHandle);
				const res = escapeFn(str);
				return vm.newString(res);
			});
			const includeHandle = vm.undefined;
			const rethrowHandle = vm.newFunction('rethrow',
				(errHandle, strHandle, flnmHandle, linenoHandle) => {
					const str = vm.getString(strHandle);
					const flnm = vm.getString(flnmHandle);
					const lineno = vm.getNumber(linenoHandle);
					const errMsg = vm.getProp(errHandle, 'message');
					const errName = vm.getProp(errHandle, 'name');
					const errStack = vm.getProp(errHandle, 'stack');
					const err = new Error();
					if(errMsg !== vm.undefined) err.message = vm.getString(errMsg);
					if(errName !== vm.undefined) err.name = vm.getString(errName);
					if(errStack !== vm.undefined) err.stack = vm.getString(errStack);
					rethrow(err, str, flnm, lineno, escapeFn);
				}
			);
			// execute our function
			const ret = vm.callFunction(
				newFunc, vm.undefined, localsHandle,
				escapeFnHandle, includeHandle, rethrowHandle
			);
			// dispose of everything to prevent memory leaks
			escapeFnHandle.dispose();
			rethrowHandle.dispose();
			for(const handle of handleList){
				handle.dispose();
			}
			newFunc.dispose();
			// return or throw an error
			const res = vm.unwrapResult(ret);
			const str = vm.getString(res);
			res.dispose();
			return str;
		};
	};

	const wrapValue = function(value, makeDisposable){
		if(value === undefined) return vm.undefined;
		if(value === null) return vm.null;
		if(typeof value === "boolean") return value ? vm.true : vm.false;
		if(typeof value === "number") return makeDisposable( vm.newNumber(value) );
		if(Array.isArray(value)){
			const arr = makeDisposable( vm.newArray() );
			for(let i = (value.length - 1); i >= 0; i--)
				vm.setProp(arr, i, wrapValue(value[i], makeDisposable));
			return arr;
		}
		if(typeof value === "object"){
			const obj = makeDisposable( vm.newObject() );
			for(const key in value)
				vm.setProp(obj, key, wrapValue(value[key], makeDisposable));
			return obj;
		}
		if(value.toString) return makeDisposable( vm.newString(value.toString()) );
		return vm.undefined;
	};
}

async function LoadSafeEJS(){
	return new SafeEJS(await getQuickJS());
}

export default LoadSafeEJS;

ncake avatar Jan 15 '23 13:01 ncake

can we get this merged?

ralyodio avatar Sep 03 '23 11:09 ralyodio

Can this get merged? I would love to use quickjs-sandboxed templates in my app

teoboley avatar Aug 21 '24 13:08 teoboley

@mde pls

teoboley avatar Aug 26 '24 14:08 teoboley