/* * Copyright (C) 2020-2021 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ const fs = require('fs'); const vm = require('vm'); const path = require('path'); const assert = require('assert'); const crypto = require('crypto'); function getContextEnvironmentRecords(context, names) { // Enables export of values from the declarative environment record if (!Array.isArray(names) || names.length === 0) { return []; } let scriptSource = '(() => {\n "use strict";\n const results = [];'; for (const name of names) { scriptSource += `\n try { results.push(${name}); } catch (e) { results.push(void 0); }`; } scriptSource += '\n return results;\n})();'; const script = new vm.Script(scriptSource, {filename: 'getContextEnvironmentRecords'}); const contextHasNames = Object.prototype.hasOwnProperty.call(context, 'names'); const contextNames = context.names; context.names = names; const results = script.runInContext(context, {}); if (contextHasNames) { context.names = contextNames; } else { delete context.names; } return Array.from(results); } function isDeepStrictEqual(val1, val2) { if (val1 === val2) { return true; } if (Array.isArray(val1)) { if (Array.isArray(val2)) { return isArrayDeepStrictEqual(val1, val2); } } else if (typeof val1 === 'object' && val1 !== null) { if (typeof val2 === 'object' && val2 !== null) { return isObjectDeepStrictEqual(val1, val2); } } return false; } function isArrayDeepStrictEqual(val1, val2) { const ii = val1.length; if (ii !== val2.length) { return false; } for (let i = 0; i < ii; ++i) { if (!isDeepStrictEqual(val1[i], val2[i])) { return false; } } return true; } function isObjectDeepStrictEqual(val1, val2) { const keys1 = Object.keys(val1); const keys2 = Object.keys(val2); if (keys1.length !== keys2.length) { return false; } const keySet = new Set(keys1); for (const key of keys2) { if (!keySet.delete(key)) { return false; } } for (const key of keys1) { if (!isDeepStrictEqual(val1[key], val2[key])) { return false; } } const tag1 = Object.prototype.toString.call(val1); const tag2 = Object.prototype.toString.call(val2); if (tag1 !== tag2) { return false; } return true; } function deepStrictEqual(actual, expected) { try { // This will fail on prototype === comparison on cross context objects assert.deepStrictEqual(actual, expected); } catch (e) { if (!isDeepStrictEqual(actual, expected)) { throw e; } } } function createURLClass(urlMap) { const BaseURL = URL; const result = function URL(url) { const u = new BaseURL(url); this.hash = u.hash; this.host = u.host; this.hostname = u.hostname; this.href = u.href; this.origin = u.origin; this.password = u.password; this.pathname = u.pathname; this.port = u.port; this.protocol = u.protocol; this.search = u.search; this.searchParams = u.searchParams; this.username = u.username; }; result.createObjectURL = (object) => { const id = crypto.randomBytes(16).toString('hex'); const url = `blob:${id}`; urlMap.set(url, object); return url; }; result.revokeObjectURL = (url) => { urlMap.delete(url); }; return result; } class VM { constructor(context={}) { this._urlMap = new Map(); context.URL = createURLClass(this._urlMap); this._context = vm.createContext(context); this._assert = { deepStrictEqual }; } get context() { return this._context; } get assert() { return this._assert; } get(names) { if (typeof names === 'string') { return getContextEnvironmentRecords(this._context, [names])[0]; } else if (Array.isArray(names)) { return getContextEnvironmentRecords(this._context, names); } else { throw new Error('Invalid argument'); } } set(values) { if (typeof values === 'object' && values !== null) { Object.assign(this._context, values); } else { throw new Error('Invalid argument'); } } execute(fileNames) { const single = !Array.isArray(fileNames); if (single) { fileNames = [fileNames]; } const results = []; for (const fileName of fileNames) { const absoluteFileName = path.resolve(__dirname, '..', 'ext', fileName); const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'}); const script = new vm.Script(source, {filename: absoluteFileName}); results.push(script.runInContext(this._context, {})); } return single ? results[0] : results; } getUrlObject(url) { return this._urlMap.get(url); } } module.exports = { VM };