UNPKG

distinctiomagnam

Version:
419 lines (364 loc) 10.4 kB
import traverse, { ExitCallback } from "../traverse"; import { AddComment, Node } from "../util/gen"; import { alphabeticalGenerator, choice, getRandomInteger, } from "../util/random"; import { ok } from "assert"; import Obfuscator from "../obfuscator"; import { ObfuscateOptions } from "../options"; import { ComputeProbabilityMap } from "../probability"; import { reservedIdentifiers, reservedKeywords } from "../constants"; import { ObfuscateOrder } from "../order"; /** * Base-class for all transformations. * - Transformations can have preparation transformations `.before` * - Transformations can have cleanup transformations `.after` * * - `match()` function returns true/false if possible candidate * - `transform()` function modifies the object * * ```js * class Example extends Transform { * constructor(o){ * super(o); * } * * match(object, parents){ * return object.type == "..."; * } * * transform(object, parents){ * // onEnter * * return ()=>{ * // onExit * } * } * * apply(tree){ * // onStart * * super.apply(tree); * * // onEnd * } * } * ``` */ export default class Transform { /** * The obfuscator. */ obfuscator: Obfuscator; /** * The user's options. */ options: ObfuscateOptions; /** * Only required for top-level transformations. */ priority: number; /** * Transforms to run before, such as `Variable Analysis`. */ before: Transform[]; /** * Transforms to run after. */ after: Transform[]; /** * Transformations to run at the same time (can cause conflicts so use sparingly) */ concurrent: Transform[]; constructor(obfuscator, priority: number = -1) { ok(obfuscator instanceof Obfuscator, "obfuscator should be an Obfuscator"); this.obfuscator = obfuscator; this.options = this.obfuscator.options; this.priority = priority; this.before = []; this.after = []; this.concurrent = []; } /** * The transformation name. */ get className() { return ( ObfuscateOrder[this.priority] || (this as any).__proto__.constructor.name ); } /** * Run an AST through the transformation (including `pre` and `post` transforms) * @param tree */ apply(tree: Node) { if (tree.type == "Program" && this.options.verbose) { if (this.priority === -1) { console.log("#", ">", this.className); } else { console.log("#", this.priority, this.className); } } /** * Run through pre-transformations */ this.before.forEach((x) => x.apply(tree)); traverse(tree, (object, parents) => { var fns = []; fns.push(this.input(object, parents)); // Fix 1. Increase performance with multiple transforms on one iteration. this.concurrent.forEach((x) => fns.push(x.input(object, parents))); return () => fns.forEach((x) => x && x()); }); /** * Cleanup transformations */ this.after.forEach((x) => x.apply(tree)); } /** * The `match` function filters for possible candidates. * * - If `true`, the node is sent to the `transform()` method * - else it's discarded. * * @param object * @param parents * @param block */ match(object: Node, parents: Node[]): boolean { throw new Error("not implemented"); } /** * Modifies the given node. * * - Return a function to be ran when the node is exited. * - The node is safe to modify in most cases. * * @param object - Current node * @param parents - Array of ancestors `[Closest, ..., Root]` * @param block */ transform(object: Node, parents: Node[]): ExitCallback | void { throw new Error("not implemented"); } /** * Calls `.match` with the given parameters, and then `.transform` if satisfied. * @private */ input(object: Node, parents: Node[]): ExitCallback | void { if (this.match(object, parents)) { return this.transform(object, parents); } } /** * Returns a random string. * * Used for creating temporary variables names, typically before RenameVariables has ran. * * These long temp names will be converted to short, mangled names by RenameVariables. */ getPlaceholder() { const genRanHex = (size) => [...Array(size)] .map(() => Math.floor(Math.random() * 10).toString(10)) .join(""); return "__p_" + genRanHex(10); } /** * Returns an independent name generator with it's own counter. * @param offset * @returns */ getGenerator(offset = 0) { var count = offset; return { generate: () => { count++; return this.generateIdentifier(-1, count); }, }; } /** * Generates a valid variable name. * @param length Default length is 6 to 10 characters. * @returns **`string`** */ generateIdentifier(length: number = -1, count = -1): string { if (length == -1) { length = getRandomInteger(6, 8); } var set = new Set(); if (count == -1) { this.obfuscator.varCount++; count = this.obfuscator.varCount; set = this.obfuscator.generated; } var identifier; do { identifier = ComputeProbabilityMap( this.options.identifierGenerator, (mode = "randomized") => { switch (mode) { case "randomized": var characters = "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split( "" ); var numbers = "0123456789".split(""); var combined = [...characters, ...numbers]; var result = ""; for (var i = 0; i < length; i++) { result += choice(i == 0 ? characters : combined); } return result; case "hexadecimal": const genRanHex = (size) => [...Array(size)] .map(() => Math.floor(Math.random() * 16).toString(16)) .join(""); return "_0x" + genRanHex(length).toUpperCase(); case "mangled": while (1) { var result = alphabeticalGenerator(count); count++; if ( reservedKeywords.has(result) || reservedIdentifiers.has(result) ) { } else { return result; } } throw new Error("impossible but TypeScript insists"); case "number": return "var_" + count; case "zeroWidth": var keyWords = [ "if", "in", "for", "let", "new", "try", "var", "case", "else", "null", "break", "catch", "class", "const", "super", "throw", "while", "yield", "delete", "export", "import", "public", "return", "switch", "default", "finally", "private", "continue", "debugger", "function", "arguments", "protected", "instanceof", "function", "await", "async", ]; var safe = "\u200C".repeat(count + 1); var base = choice(keyWords) + safe; return base; } throw new Error("Invalid 'identifierGenerator' mode: " + mode); } ); } while (set.has(identifier)); if (!identifier) { throw new Error("identifier null"); } set.add(identifier); return identifier; } /** * Smartly appends a comment to a Node. * - Includes the transformation's name. * @param node * @param text * @param i */ addComment(node: Node, text: string) { if (this.options.debugComments) { return AddComment(node, `[${this.className}] ${text}`); } return node; } replace(node1: Node, node2: Node) { for (var key in node1) { delete node1[key]; } this.objectAssign(node1, node2); } replaceIdentifierOrLiteral(node1: Node, node2: Node, parents: Node[]) { // Fix 2. Make parent property key computed if ( parents[0] && (parents[0].type == "Property" || parents[0].type == "MethodDefinition") && parents[0].key == node1 ) { parents[0].computed = true; parents[0].shorthand = false; } this.replace(node1, node2); } /** * Smartly merges two Nodes. * - Null checking * - Preserves comments * @param node1 * @param node2 */ objectAssign(node1: Node, node2: Node): Node { ok(node1); ok(node2); var comments1 = node1.leadingComments || []; var comments2 = node2.leadingComments || []; var comments = [...comments1, ...comments2]; node2.leadingComments = comments; node1._transform = node2._transform = this.className; return Object.assign(node1, node2); } /** * Verbose logging for this transformation. * @param messages */ log(...messages: any[]) { if (this.options.verbose) { console.log("[" + this.className + "]", ...messages); } } /** * Verbose logging for warning/importing messages. * @param messages */ warn(...messages: any[]) { if (this.options.verbose) { console.log("[ WARN " + this.className + " ]", ...messages); } } /** * Throws an error. Appends the transformation's name to the error's message. * @param error */ error(error: Error): never { throw new Error(`${this.className} Error: ${error.message}`); } }