"use strict"; const acorn = require("acorn"); const escodegen = require("@javascript-obfuscator/escodegen"); const vm = require("vm"); const fs = require("fs"); const path = require("path"); const PDFJS_PREPROCESSOR_NAME = "PDFJSDev"; const ROOT_PREFIX = "$ROOT/"; const ACORN_ECMA_VERSION = 2022; function isLiteral(obj, value) { return obj.type === "Literal" && obj.value === value; } function isPDFJSPreprocessor(obj) { return obj.type === "Identifier" && obj.name === PDFJS_PREPROCESSOR_NAME; } function evalWithDefines(code, defines, loc) { if (!code || !code.trim()) { throw new Error("No JavaScript expression given"); } return vm.runInNewContext(code, defines, { displayErrors: false }); } function handlePreprocessorAction(ctx, actionName, args, loc) { try { let arg; switch (actionName) { case "test": arg = args[0]; if (!arg || arg.type !== "Literal" || typeof arg.value !== "string") { throw new Error("No code for testing is given"); } const isTrue = !!evalWithDefines(arg.value, ctx.defines); return { type: "Literal", value: isTrue, loc }; case "eval": arg = args[0]; if (!arg || arg.type !== "Literal" || typeof arg.value !== "string") { throw new Error("No code for eval is given"); } const result = evalWithDefines(arg.value, ctx.defines); if ( typeof result === "boolean" || typeof result === "string" || typeof result === "number" ) { return { type: "Literal", value: result, loc }; } if (typeof result === "object") { const parsedObj = acorn.parse("(" + JSON.stringify(result) + ")", { ecmaVersion: ACORN_ECMA_VERSION, }); parsedObj.body[0].expression.loc = loc; return parsedObj.body[0].expression; } break; case "json": arg = args[0]; if (!arg || arg.type !== "Literal" || typeof arg.value !== "string") { throw new Error("Path to JSON is not provided"); } let jsonPath = arg.value; if (jsonPath.indexOf(ROOT_PREFIX) === 0) { jsonPath = path.join( ctx.rootPath, jsonPath.substring(ROOT_PREFIX.length) ); } const jsonContent = fs.readFileSync(jsonPath).toString(); const parsedJSON = acorn.parse("(" + jsonContent + ")", { ecmaVersion: ACORN_ECMA_VERSION, }); parsedJSON.body[0].expression.loc = loc; return parsedJSON.body[0].expression; } throw new Error("Unsupported action"); } catch (e) { throw new Error( "Could not process " + PDFJS_PREPROCESSOR_NAME + "." + actionName + " at " + JSON.stringify(loc) + "\n" + e.name + ": " + e.message ); } } function postprocessNode(ctx, node) { switch (node.type) { case "ExportNamedDeclaration": case "ImportDeclaration": if ( node.source && node.source.type === "Literal" && ctx.map && ctx.map[node.source.value] ) { const newValue = ctx.map[node.source.value]; node.source.value = node.source.raw = newValue; } break; case "IfStatement": if (isLiteral(node.test, true)) { // if (true) stmt1; => stmt1 return node.consequent; } else if (isLiteral(node.test, false)) { // if (false) stmt1; else stmt2; => stmt2 return node.alternate || { type: "EmptyStatement", loc: node.loc }; } break; case "ConditionalExpression": if (isLiteral(node.test, true)) { // true ? stmt1 : stmt2 => stmt1 return node.consequent; } else if (isLiteral(node.test, false)) { // false ? stmt1 : stmt2 => stmt2 return node.alternate; } break; case "UnaryExpression": if (node.operator === "typeof" && isPDFJSPreprocessor(node.argument)) { // typeof PDFJSDev => 'object' return { type: "Literal", value: "object", loc: node.loc }; } if ( node.operator === "!" && node.argument.type === "Literal" && typeof node.argument.value === "boolean" ) { // !true => false, !false => true return { type: "Literal", value: !node.argument.value, loc: node.loc }; } break; case "LogicalExpression": switch (node.operator) { case "&&": if (isLiteral(node.left, true)) { return node.right; } if (isLiteral(node.left, false)) { return node.left; } break; case "||": if (isLiteral(node.left, true)) { return node.left; } if (isLiteral(node.left, false)) { return node.right; } break; } break; case "BinaryExpression": switch (node.operator) { case "==": case "===": case "!=": case "!==": if ( node.left.type === "Literal" && node.right.type === "Literal" && typeof node.left.value === typeof node.right.value ) { // folding two literals == and != check switch (typeof node.left.value) { case "string": case "boolean": case "number": const equal = node.left.value === node.right.value; return { type: "Literal", value: (node.operator[0] === "=") === equal, loc: node.loc, }; } } break; } break; case "CallExpression": if ( node.callee.type === "MemberExpression" && isPDFJSPreprocessor(node.callee.object) && node.callee.property.type === "Identifier" ) { // PDFJSDev.xxxx(arg1, arg2, ...) => transform const action = node.callee.property.name; return handlePreprocessorAction(ctx, action, node.arguments, node.loc); } // require('string') if ( node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length === 1 && node.arguments[0].type === "Literal" && ctx.map && ctx.map[node.arguments[0].value] ) { const requireName = node.arguments[0]; requireName.value = requireName.raw = ctx.map[requireName.value]; } break; case "BlockStatement": let subExpressionIndex = 0; while (subExpressionIndex < node.body.length) { switch (node.body[subExpressionIndex].type) { case "EmptyStatement": // Removing empty statements from the blocks. node.body.splice(subExpressionIndex, 1); continue; case "BlockStatement": // Block statements inside a block are moved to the parent one. const subChildren = node.body[subExpressionIndex].body; Array.prototype.splice.apply(node.body, [ subExpressionIndex, 1, ...subChildren, ]); subExpressionIndex += Math.max(subChildren.length - 1, 0); continue; case "ReturnStatement": case "ThrowStatement": // Removing dead code after return or throw. node.body.splice( subExpressionIndex + 1, node.body.length - subExpressionIndex - 1 ); break; } subExpressionIndex++; } break; case "FunctionDeclaration": case "FunctionExpression": const block = node.body; if ( block.body.length > 0 && block.body[block.body.length - 1].type === "ReturnStatement" && !block.body[block.body.length - 1].argument ) { // Function body ends with return without arg -- removing it. block.body.pop(); } break; } return node; } function fixComments(ctx, node) { if (!ctx.saveComments) { return; } // Fixes double comments in the escodegen output. delete node.trailingComments; // Removes ESLint and other service comments. if (node.leadingComments) { const CopyrightRegExp = /\bcopyright\b/i; const BlockCommentRegExp = /^\s*(globals|eslint|falls through)\b/; const LineCommentRegExp = /^\s*eslint\b/; let i = 0; while (i < node.leadingComments.length) { const type = node.leadingComments[i].type; const value = node.leadingComments[i].value; if (ctx.saveComments === "copyright") { // Remove all comments, except Copyright notices and License headers. if (!(type === "Block" && CopyrightRegExp.test(value))) { node.leadingComments.splice(i, 1); continue; } } else if ( (type === "Block" && BlockCommentRegExp.test(value)) || (type === "Line" && LineCommentRegExp.test(value)) ) { node.leadingComments.splice(i, 1); continue; } i++; } } } function traverseTree(ctx, node) { // generic node processing for (const i in node) { const child = node[i]; if (typeof child === "object" && child !== null && child.type) { const result = traverseTree(ctx, child); if (result !== child) { node[i] = result; } } else if (Array.isArray(child)) { child.forEach(function (childItem, index) { if ( typeof childItem === "object" && childItem !== null && childItem.type ) { const result = traverseTree(ctx, childItem); if (result !== childItem) { child[index] = result; } } }); } } node = postprocessNode(ctx, node) || node; fixComments(ctx, node); return node; } function preprocessPDFJSCode(ctx, code) { const format = ctx.format || { indent: { style: " ", }, }; const parseOptions = { ecmaVersion: ACORN_ECMA_VERSION, locations: true, sourceFile: ctx.sourceFile, sourceType: "module", }; const codegenOptions = { format, parse(input) { return acorn.parse(input, { ecmaVersion: ACORN_ECMA_VERSION }); }, sourceMap: ctx.sourceMap, sourceMapWithCode: ctx.sourceMap, }; const syntax = acorn.parse(code, parseOptions); traverseTree(ctx, syntax); return escodegen.generate(syntax, codegenOptions); } exports.preprocessPDFJSCode = preprocessPDFJSCode;