From a5d69248dd82510c99d6179e22d98e0bdaa8a609 Mon Sep 17 00:00:00 2001 From: theit8514 Date: Mon, 3 Jan 2022 10:20:46 -0500 Subject: [PATCH] Replace regex import with acorn AST parser --- src/NetscriptJSEvaluator.ts | 70 +++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index d4c7b6dbc..5fba53557 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -1,3 +1,10 @@ +/** + * Uses the acorn.js library to parse a script's code into an AST and + * recursively walk through that AST to replace import urls with blobs + */ +import * as walk from "acorn-walk"; +import { parse } from "acorn"; + import { makeRuntimeRejectMsg } from "./NetscriptEvaluator"; import { ScriptUrl } from "./Script/ScriptUrl"; import { WorkerScript } from "./Netscript/WorkerScript"; @@ -125,32 +132,51 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri // import {foo} from "blob://" // // Where the blob URL contains the script content. - let transformedCode = script.code.replace( - /((?:from|import)\s+(?:'|"))(?:\.\/)?([^'"]+)('|")/g, - (unmodified, prefix, filename, suffix) => { - const isAllowedImport = scripts.some((s) => areImportsEquals(s.filename, filename)); - if (!isAllowedImport) return unmodified; - // Find the corresponding script. - const [importedScript] = scripts.filter((s) => areImportsEquals(s.filename, filename)); + // Parse the code into an ast tree + const ast: any = parse(script.code, { sourceType: "module", ecmaVersion: "latest", ranges: true }); - // Check to see if the urls for this script are stored in the cache by the hash value. - let urls = ImportCache.get(importedScript.hash()); - // If we don't have it in the cache, then we need to generate the urls for it. - if (!urls) { - // Try to get a URL for the requested script and its dependencies. - urls = _getScriptUrls(importedScript, scripts, seen); - } + const importNodes: Array = []; + // Walk the nodes of this tree and find any import declaration statements. + walk.simple(ast, { + ImportDeclaration(node: any) { + // Push this import onto the stack to replace + importNodes.push({ + filename: node.source.value, + start: node.source.range[0] + 1, + end: node.source.range[1] - 1 + }); + } + }); + // Sort the nodes from last start index to first. This replaces the last import with a blob first, + // preventing the ranges for other imports from being shifted. + importNodes.sort((a, b) => b.start - a.start); + let transformedCode = script.code; + // Loop through each node and replace the script name with a blob url. + for (const node of importNodes) { + const filename = node.filename.startsWith("./") ? node.filename.substring(2) : node.filename; - // The top url in the stack is the replacement import file for this script. - urlStack.push(...urls); - const blob = urls[urls.length - 1].url; - ImportCache.store(importedScript.hash(), urls); + // Find the corresponding script. + const matchingScripts = scripts.filter((s) => areImportsEquals(s.filename, filename)); + if (matchingScripts.length === 0) continue; - // Replace the blob inside the import statement. - return [prefix, blob, suffix].join(""); - }, - ); + const [importedScript] = matchingScripts; + // Check to see if the urls for this script are stored in the cache by the hash value. + let urls = ImportCache.get(importedScript.hash()); + // If we don't have it in the cache, then we need to generate the urls for it. + if (!urls) { + // Try to get a URL for the requested script and its dependencies. + urls = _getScriptUrls(importedScript, scripts, seen); + } + + // The top url in the stack is the replacement import file for this script. + urlStack.push(...urls); + const blob = urls[urls.length - 1].url; + ImportCache.store(importedScript.hash(), urls); + + // Replace the blob inside the import statement. + transformedCode = transformedCode.substring(0, node.start) + blob + transformedCode.substring(node.end); + } // We automatically define a print function() in the NetscriptJS module so that // accidental calls to window.print() do not bring up the "print screen" dialog