Bladeren bron

Added `numbersToExpressions` option and NumberToNumericalExpressionTransformer

sanex3339 4 jaren geleden
bovenliggende
commit
dd8dcac223

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 Change Log
 
+v1.6.0
+---
+* **New option:** `numbersToExpressions` enables numbers conversion to expressions
+
 v1.5.2
 ---
 * Prevented mutation of the name sequences of `mangled` identifier name generators

+ 19 - 0
README.md

@@ -329,6 +329,7 @@ Following options are available for the JS Obfuscator:
     identifiersPrefix: '',
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],
@@ -375,6 +376,7 @@ Following options are available for the JS Obfuscator:
     --identifiers-dictionary '<list>' (comma separated)
     --identifiers-prefix <string>
     --log <boolean>
+    --numbers-to-expressions <boolean>
     --rename-globals <boolean>
     --rename-properties <boolean>
     --reserved-names '<list>' (comma separated)
@@ -667,6 +669,20 @@ Type: `boolean` Default: `false`
 
 Enables logging of the information to the console.
 
+### `numbersToExpressions`
+Type: `boolean` Default: `false`
+
+Enables numbers conversion to expressions
+
+Example: 
+```ts
+// input
+const foo = 1234;
+
+// output
+const foo=-0xd93+-0x10b4+0x41*0x67+0x84e*0x3+-0xff8;
+```
+
 ### `renameGlobals`
 Type: `boolean` Default: `false`
 
@@ -985,6 +1001,7 @@ Performance will 50-100% slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: true,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
@@ -1016,6 +1033,7 @@ Performance will 30-35% slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
@@ -1045,6 +1063,7 @@ Performance will slightly slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,

File diff suppressed because it is too large
+ 0 - 0
dist/index.browser.js


File diff suppressed because it is too large
+ 0 - 12460
dist/index.cli.js


File diff suppressed because it is too large
+ 0 - 11756
dist/index.js


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "javascript-obfuscator",
-  "version": "1.5.2",
+  "version": "1.6.0",
   "description": "JavaScript obfuscator",
   "keywords": [
     "obfuscator",

+ 1 - 0
src/JavaScriptObfuscator.ts

@@ -75,6 +75,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator {
         NodeTransformer.MemberExpressionTransformer,
         NodeTransformer.MetadataTransformer,
         NodeTransformer.MethodDefinitionTransformer,
+        NodeTransformer.NumberToNumericalExpressionTransformer,
         NodeTransformer.ObfuscatingGuardsTransformer,
         NodeTransformer.ObjectExpressionKeysTransformer,
         NodeTransformer.ObjectExpressionTransformer,

+ 35 - 30
src/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.ts

@@ -7,15 +7,17 @@ import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 
 import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
+import { NumberUtils } from '../../utils/NumberUtils';
+
 /**
- * Rework of https://gist.github.com/da411d/0e59f79dcf4603cdabf0024a10eeb6fe
+ * Based on https://gist.github.com/da411d/0e59f79dcf4603cdabf0024a10eeb6fe
  */
 @injectable()
 export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpressionAnalyzer {
     /**
      * @type {number}
      */
-    private static readonly additionalParts: number = 5;
+    private static readonly additionalParts: number = 3;
 
     /**
      * @type {IRandomGenerator}
@@ -40,6 +42,10 @@ export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpres
             throw new Error('Given value is NaN');
         }
 
+        if (NumberUtils.isUnsafeNumber(number)) {
+            return [number];
+        }
+
         const additionParts: number[] = this.generateAdditionParts(number);
 
         return additionParts.map((addition: number) => this.mixWithMultiplyParts(addition));
@@ -52,8 +58,10 @@ export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpres
     private generateAdditionParts (number: number): number[] {
         const additionParts = [];
 
-        const from: number = Math.min(-10000, -Math.abs(number * 2));
-        const to: number = Math.max(10000, Math.abs(number * 2));
+        const upperNumberLimit: number = Math.min(Math.abs(number * 2), Number.MAX_SAFE_INTEGER);
+
+        const from: number = Math.min(-10000, -upperNumberLimit);
+        const to: number = Math.max(10000, upperNumberLimit);
 
         let temporarySum = 0;
 
@@ -61,14 +69,27 @@ export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpres
             if (i < NumberNumericalExpressionAnalyzer.additionalParts - 1) {
                 // trailing parts
 
-                const addition: number = this.randomGenerator.getRandomInteger(from, to);
+                let addition: number = this.randomGenerator.getRandomInteger(from, to);
+                const isUnsafeCombination: boolean = NumberUtils.isUnsafeNumber(temporarySum + addition);
+
+                // we have to flip sign if total expression sum overflows over safe integer limits
+                if (isUnsafeCombination) {
+                    addition = -addition;
+                }
 
                 additionParts.push(addition);
                 temporarySum += addition;
             } else {
-                // last part
+                const combination: number = number - temporarySum;
+                const isUnsafeCombination: boolean = NumberUtils.isUnsafeNumber(combination);
 
-                additionParts.push(number - temporarySum);
+                // last part
+                if (isUnsafeCombination) {
+                    additionParts.push(0 - temporarySum);
+                    additionParts.push(number);
+                } else {
+                    additionParts.push(combination);
+                }
             }
         }
 
@@ -80,36 +101,20 @@ export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpres
      * @returns {number | number[]}
      */
     private mixWithMultiplyParts (number: number): number | number[] {
-        const dividers: number[] = this.getDividers(number);
-
         const shouldMixWithMultiplyParts: boolean = this.randomGenerator.getMathRandom() > 0.5;
 
-        if (!shouldMixWithMultiplyParts || !dividers.length) {
+        if (!shouldMixWithMultiplyParts) {
             return number;
         }
 
-        const divider = dividers[
-            this.randomGenerator.getRandomInteger(0, dividers.length - 1)
-        ];
-
-        return [divider, number / divider];
-    }
+        const factors: number[] = NumberUtils.getFactors(number);
 
-    /**
-     * @param {number} number
-     * @returns {number[]}
-     */
-    private getDividers (number: number): number[] {
-        const dividers: number[] = [];
-
-        number = Math.abs(number);
-
-        for (let i = 2; i < number; i++) {
-            if (number % i === 0){
-                dividers.push(i);
-            }
+        if (!factors.length) {
+            return number;
         }
 
-        return dividers;
+        const factor: number = factors[this.randomGenerator.getRandomInteger(0, factors.length - 1)];
+
+        return [factor, number / factor];
     }
 }

+ 4 - 0
src/cli/JavaScriptObfuscatorCLI.ts

@@ -249,6 +249,10 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 '--log <boolean>', 'Enables logging of the information to the console',
                 BooleanSanitizer
             )
+            .option(
+                '--numbers-to-expressions <boolean>', 'Enables numbers conversion to expressions',
+                BooleanSanitizer
+            )
             .option(
                 '--reserved-names <list> (comma separated, without whitespaces)',
                 'Disables obfuscation and generation of identifiers, which being matched by passed RegExp patterns (comma separated)',

+ 7 - 1
src/container/modules/node-transformers/ConvertingTransformersModule.ts

@@ -8,14 +8,15 @@ import { IObjectExpressionExtractor } from '../../../interfaces/node-transformer
 import { NodeTransformer } from '../../../enums/node-transformers/NodeTransformer';
 import { ObjectExpressionExtractor } from '../../../enums/node-transformers/converting-transformers/properties-extractors/ObjectExpressionExtractor';
 
+import { BasePropertiesExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor';
 import { ObjectExpressionToVariableDeclarationExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/ObjectExpressionToVariableDeclarationExtractor';
 import { MemberExpressionTransformer } from '../../../node-transformers/converting-transformers/MemberExpressionTransformer';
 import { MethodDefinitionTransformer } from '../../../node-transformers/converting-transformers/MethodDefinitionTransformer';
+import { NumberToNumericalExpressionTransformer } from '../../../node-transformers/converting-transformers/NumberToNumericalExpressionTransformer';
 import { ObjectExpressionKeysTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionKeysTransformer';
 import { ObjectExpressionTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionTransformer';
 import { SplitStringTransformer } from '../../../node-transformers/converting-transformers/SplitStringTransformer';
 import { TemplateLiteralTransformer } from '../../../node-transformers/converting-transformers/TemplateLiteralTransformer';
-import { BasePropertiesExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor';
 
 export const convertingTransformersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // converting transformers
@@ -27,6 +28,11 @@ export const convertingTransformersModule: interfaces.ContainerModule = new Cont
         .to(MethodDefinitionTransformer)
         .whenTargetNamed(NodeTransformer.MethodDefinitionTransformer);
 
+
+    bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
+        .to(NumberToNumericalExpressionTransformer)
+        .whenTargetNamed(NodeTransformer.NumberToNumericalExpressionTransformer);
+
     bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
         .to(ObjectExpressionKeysTransformer)
         .whenTargetNamed(NodeTransformer.ObjectExpressionKeysTransformer);

+ 1 - 0
src/enums/node-transformers/NodeTransformer.ts

@@ -11,6 +11,7 @@ export enum NodeTransformer {
     MemberExpressionTransformer = 'MemberExpressionTransformer',
     MetadataTransformer = 'MetadataTransformer',
     MethodDefinitionTransformer = 'MethodDefinitionTransformer',
+    NumberToNumericalExpressionTransformer = 'NumberToNumericalExpressionTransformer',
     ObfuscatingGuardsTransformer = 'ObfuscatingGuardsTransformer',
     ObjectExpressionKeysTransformer = 'ObjectExpressionKeysTransformer',
     ObjectExpressionTransformer = 'ObjectExpressionTransformer',

+ 1 - 0
src/interfaces/options/IOptions.ts

@@ -21,6 +21,7 @@ export interface IOptions {
     readonly identifiersPrefix: string;
     readonly inputFileName: string;
     readonly log: boolean;
+    readonly numbersToExpressions: boolean;
     readonly renameGlobals: boolean;
     readonly renameProperties: boolean;
     readonly reservedNames: string[];

+ 175 - 0
src/node-transformers/converting-transformers/NumberToNumericalExpressionTransformer.ts

@@ -0,0 +1,175 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { TNumberNumericalExpressionData } from '../../types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData';
+
+import { INumberNumericalExpressionAnalyzer } from '../../interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
+
+import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
+
+import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
+import { NodeGuards } from '../../node/NodeGuards';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NumberUtils } from '../../utils/NumberUtils';
+
+/**
+ * replaces:
+ *     var number = 123;
+ *
+ * on:
+ *     var number = 50 + (100 * 2) - 127;
+ */
+@injectable()
+export class NumberToNumericalExpressionTransformer extends AbstractNodeTransformer {
+    /**
+     * @type {INumberNumericalExpressionAnalyzer}
+     */
+    private readonly numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer;
+
+    /**
+     * @param {INumberNumericalExpressionAnalyzer} numberNumericalExpressionAnalyzer
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.INumberNumericalExpressionAnalyzer)
+            numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(randomGenerator, options);
+
+        this.numberNumericalExpressionAnalyzer = numberNumericalExpressionAnalyzer;
+    }
+
+    /**
+     * @param {NodeTransformationStage} nodeTransformationStage
+     * @returns {IVisitor | null}
+     */
+    public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
+        if (!this.options.numbersToExpressions) {
+            return null;
+        }
+
+        switch (nodeTransformationStage) {
+            case NodeTransformationStage.Converting:
+                return {
+                    leave: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
+                        if (parentNode && NodeGuards.isLiteralNode(node)) {
+                            return this.transformNode(node, parentNode);
+                        }
+                    }
+                };
+
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * @param {Literal} literalNode
+     * @param {NodeGuards} parentNode
+     * @returns {NodeGuards}
+     */
+    public transformNode (literalNode: ESTree.Literal, parentNode: ESTree.Node): ESTree.Node {
+        if (typeof literalNode.value !== 'number') {
+            return literalNode;
+        }
+
+        if (NodeGuards.isPropertyNode(parentNode) && !parentNode.computed) {
+            return literalNode;
+        }
+
+        const numberNumericalExpressionData: TNumberNumericalExpressionData = this.numberNumericalExpressionAnalyzer.analyze(literalNode.value);
+
+        return this.convertNumericalExpressionDataToNode(numberNumericalExpressionData);
+    }
+
+    /**
+     * @param {TNumberNumericalExpressionData} numberNumericalExpressionData
+     * @param {ESTree.BinaryOperator} operator
+     * @returns {ESTree.BinaryExpression | ESTree.Literal | ESTree.UnaryExpression}
+     */
+    private convertNumericalExpressionDataToNode (
+        numberNumericalExpressionData: TNumberNumericalExpressionData,
+        operator: ESTree.BinaryOperator = '+'
+    ): ESTree.BinaryExpression | ESTree.Literal | ESTree.UnaryExpression {
+        const numberNumericalExpressionDataLength: number = numberNumericalExpressionData.length;
+
+        const leftParts: TNumberNumericalExpressionData = numberNumericalExpressionDataLength > 1
+            ? numberNumericalExpressionData.slice(0, numberNumericalExpressionDataLength - 1)
+            : [numberNumericalExpressionData[0]];
+        const rightParts: TNumberNumericalExpressionData = numberNumericalExpressionDataLength > 1
+            ? numberNumericalExpressionData.slice(-1)
+            : [];
+
+        // trailing iterations
+        if (rightParts.length) {
+            return this.convertPartsToBinaryExpression(operator, leftParts, rightParts);
+        }
+
+        const firstLeftPartOrNumber: number | number[] | null = leftParts[0] ?? null;
+
+        // last iteration when only single left part is left
+        return Array.isArray(firstLeftPartOrNumber)
+            ? this.convertNumericalExpressionDataToNode(firstLeftPartOrNumber, '*')
+            : this.convertPartOrNumberToLiteralNode(firstLeftPartOrNumber);
+    }
+
+    /**
+     * @param {ESTree.BinaryOperator} operator
+     * @param {TNumberNumericalExpressionData} leftParts
+     * @param {TNumberNumericalExpressionData} rightParts
+     * @returns {ESTree.BinaryExpression}
+     */
+    private convertPartsToBinaryExpression (
+        operator: ESTree.BinaryOperator,
+        leftParts: TNumberNumericalExpressionData,
+        rightParts: TNumberNumericalExpressionData
+    ): ESTree.BinaryExpression {
+        const rightPartOrNumber: number | number[] = rightParts[0];
+
+        if (Array.isArray(rightPartOrNumber)) {
+            // right part is array with multiply numbers
+            return NodeFactory.binaryExpressionNode(
+                operator,
+                this.convertNumericalExpressionDataToNode(leftParts),
+                this.convertNumericalExpressionDataToNode(rightPartOrNumber, '*')
+            );
+        } else {
+            // right part is number
+            return NodeFactory.binaryExpressionNode(
+                operator,
+                this.convertNumericalExpressionDataToNode(leftParts),
+                this.convertPartOrNumberToLiteralNode(rightPartOrNumber)
+            );
+        }
+    }
+
+    /**
+     * @param {number | number[]} partOrNumber
+     * @returns {ESTree.Literal}
+     */
+    private convertPartOrNumberToLiteralNode (
+        partOrNumber: number | number[]
+    ): ESTree.Literal | ESTree.UnaryExpression {
+        const number: number = Array.isArray(partOrNumber)
+            ? partOrNumber[0]
+            : partOrNumber;
+        const isPositiveNumber: boolean = NumberUtils.isPositive(number);
+
+        const literalNode: ESTree.Literal = NodeFactory.literalNode(Math.abs(number));
+
+        return isPositiveNumber
+            ? literalNode
+            : NodeFactory.unaryExpressionNode(
+                '-',
+                literalNode
+            );
+    }
+}

+ 6 - 0
src/options/Options.ts

@@ -151,6 +151,12 @@ export class Options implements IOptions {
     @IsBoolean()
     public readonly log!: boolean;
 
+    /**
+     * @type {boolean}
+     */
+    @IsBoolean()
+    public readonly numbersToExpressions!: boolean;
+
     /**
      * @type {boolean}
      */

+ 1 - 0
src/options/presets/Default.ts

@@ -21,6 +21,7 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     identifiersDictionary: [],
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],

+ 1 - 0
src/options/presets/NoCustomNodes.ts

@@ -20,6 +20,7 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     identifiersDictionary: [],
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],

+ 81 - 0
src/utils/NumberUtils.ts

@@ -24,4 +24,85 @@ export class NumberUtils {
             ? number % 1 === 0
             : true;
     }
+
+    /**
+     * @param {number} number
+     * @returns {boolean}
+     */
+    public static isPositive (number: number): boolean {
+        if (isNaN(number)) {
+            throw new Error('Given value is NaN');
+        }
+
+        if (number > 0) {
+            return true;
+        }
+
+        if (number < 0) {
+            return false;
+        }
+
+        if (1 / number === Number.POSITIVE_INFINITY) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param {number} number
+     * @returns {boolean}
+     */
+    public static isUnsafeNumber (number: number): boolean {
+        if (isNaN(number)) {
+            throw new Error('Given value is NaN');
+        }
+
+        return number < Number.MIN_SAFE_INTEGER || number > Number.MAX_SAFE_INTEGER;
+    }
+
+    /**
+     * Returns all factors of a number
+     * Based on https://stackoverflow.com/a/43204663
+     *
+     * @param {number} number
+     * @returns {number[]}
+     */
+    public static getFactors (number: number): number[] {
+        if (number === 0) {
+            throw new Error('Invalid number. Allowed only non-zero number');
+        }
+
+        number = Math.abs(number);
+
+        // special case for 1
+        if (number === 1) {
+            return [-number, number];
+        }
+
+        const factors: number[] = [];
+
+        const isEven: boolean = number % 2 === 0;
+        const incrementValue: number = isEven ? 1 : 2;
+
+        for (
+            let currentFactor = 1;
+            Math.pow(currentFactor, 2) <= number;
+            currentFactor += incrementValue
+        ) {
+            if (number % currentFactor !== 0) {
+                continue;
+            }
+
+            factors.push(...[-currentFactor, currentFactor]);
+
+            const compliment: number = number / currentFactor;
+
+            if (compliment !== currentFactor) {
+                factors.push(...[-compliment, compliment]);
+            }
+        }
+
+        return factors.sort((a: number, b: number) => a - b);
+    }
 }

+ 3 - 12
test/dev/dev.ts

@@ -1,6 +1,5 @@
 'use strict';
 
-import { IdentifierNamesGenerator } from '../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
 import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNodes';
 
 (function () {
@@ -8,21 +7,13 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-            const example1 = 0;
-            const example2 = 1;
-            const example3 = 100;
-            const example4 = 125793;
-            const example5 = -15232103;
+            const foo = 100;
             
-            console.log(example1);
-            console.log(example2);
-            console.log(example3);
-            console.log(example4);
-            console.log(example5);
+            console.log(foo);
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
-            identifierNamesGenerator: IdentifierNamesGenerator.MangledShuffledIdentifierNamesGenerator
+            numbersToExpressions: true
         }
     ).getObfuscatedCode();
 

+ 167 - 0
test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/NumbersToNumericalExpressionsTransformer.spec.ts

@@ -0,0 +1,167 @@
+import { assert } from 'chai';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
+
+import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
+import { readFileAsString } from '../../../../helpers/readFileAsString';
+
+describe('NumbersToNumericalExpressionsTransformer', function () {
+    this.timeout(60000);
+
+    describe('Variant #1: base', () => {
+        const initialNumber: number = -50;
+        const lastNumber: number = 50;
+
+        let areValidExpressions: boolean = true;
+
+        before(() => {
+            for (let i = initialNumber; i < lastNumber; i++) {
+                const number: number = i;
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+
+                const result: number = eval(obfuscatedCode);
+
+                if (result !== number) {
+                    areValidExpressions = false;
+                    break;
+                }
+            }
+        });
+
+        it('should correctly transform numbers to expressions', () => {
+            assert.isTrue(areValidExpressions);
+        });
+    });
+
+    describe('Variant #2: safe integers', () => {
+        describe('Variant #1: max safe integer', () => {
+            const number: number = Number.MAX_SAFE_INTEGER;
+            const samplesCount: number = 15;
+
+            let areValidExpressions: boolean = true;
+
+            before(() => {
+                for (let i = 0; i < samplesCount; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        `${number};`,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            numbersToExpressions: true
+                        }
+                    ).getObfuscatedCode();
+
+                    const result: number = eval(obfuscatedCode);
+
+                    if (result !== number) {
+                        areValidExpressions = false;
+                        break;
+                    }
+                }
+            });
+
+            it('should correctly transform numbers to expressions', () => {
+                assert.isTrue(areValidExpressions);
+            });
+        });
+
+        describe('Variant #2: max unsafe integer', () => {
+            const unsafeIntegerRegExp: RegExp = /0x20000000000000;/;
+            const number: number = Number.MAX_SAFE_INTEGER + 1;
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should not transform unsafe integer to expressions', () => {
+                assert.match(obfuscatedCode, unsafeIntegerRegExp);
+            });
+        });
+
+        describe('Variant #3: min safe integer', () => {
+            const number: number = Number.MIN_SAFE_INTEGER;
+            const samplesCount: number = 15;
+
+            let areValidExpressions: boolean = true;
+
+            before(() => {
+                for (let i = 0; i < samplesCount; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        `${number};`,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            numbersToExpressions: true
+                        }
+                    ).getObfuscatedCode();
+
+                    const result: number = eval(obfuscatedCode);
+
+                    if (result !== number) {
+                        areValidExpressions = false;
+                        break;
+                    }
+                }
+            });
+
+            it('should correctly transform numbers to expressions', () => {
+                assert.isTrue(areValidExpressions);
+            });
+        });
+
+        describe('Variant #4: min unsafe integer', () => {
+            const unsafeIntegerRegExp: RegExp = /-0x20000000000000;/;
+            const number: number = Number.MIN_SAFE_INTEGER - 1;
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should not transform unsafe integer to expressions', () => {
+                assert.match(obfuscatedCode, unsafeIntegerRegExp);
+            });
+        });
+    });
+
+    describe('Variant #3: parent node is non-computed object property', () => {
+        const regExp: RegExp = /const foo *= *{1: *'bar'};/;
+
+        let obfuscatedCode: string;
+
+        before(() => {
+            const code: string = readFileAsString(__dirname + '/fixtures/non-computed-object-key.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    numbersToExpressions: true
+                }
+            ).getObfuscatedCode();
+        });
+
+        it('should not replace non-computed object property literal with expression', () => {
+            assert.match(obfuscatedCode,  regExp);
+        });
+    });
+});

+ 3 - 0
test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/fixtures/non-computed-object-key.js

@@ -0,0 +1,3 @@
+const foo = {
+    1: 'bar'
+};

+ 1 - 0
test/index.spec.ts

@@ -88,6 +88,7 @@ import './functional-tests/node-transformers/control-flow-transformers/control-f
 import './functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/StringLiteralControlFlowReplacer.spec';
 import './functional-tests/node-transformers/converting-transformers/member-expression-transformer/MemberExpressionTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/method-definition-transformer/MethodDefinitionTransformer.spec';
+import './functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/NumbersToNumericalExpressionsTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/object-expression-transformer/ObjectExpressionTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/split-string-transformer/SplitStringTransformer.spec';

+ 108 - 27
test/unit-tests/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.spec.ts

@@ -21,9 +21,11 @@ const numberNumericalExpressionDataToString = (data: TNumberNumericalExpressionD
         .join('+')
         .replace(/\+-/g, '-');
 
-describe('NumberNumericalExpressionAnalyzer', () => {
+describe('NumberNumericalExpressionAnalyzer', function() {
     let numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer;
 
+    this.timeout(10000);
+
     before(() => {
         const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
 
@@ -35,52 +37,131 @@ describe('NumberNumericalExpressionAnalyzer', () => {
     describe('analyze', () => {
         let evaluatedResult: number;
 
-        describe('Variant #1: positive number', () => {
-            const number: number = 1234;
+        describe('Positive numbers', () => {
+            describe('Variant #1: positive number', () => {
+                const number: number = 1234;
 
-            before(() => {
-                const numberNumericalExpressionData: TNumberNumericalExpressionData =
-                    numberNumericalExpressionAnalyzer.analyze(number);
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
 
-                evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
 
-            it('should return correct number numerical expression data', () => {
-                assert.equal(number, evaluatedResult);
+            describe('Variant #2: positive zero number', () => {
+                const number: number = 0;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
-        });
 
-        describe('Variant #2: negative number', () => {
-            const number: number = -1234;
+            describe('Variant #3: positive big number', () => {
+                const number: number = Number.MAX_SAFE_INTEGER;
 
-            before(() => {
-                const numberNumericalExpressionData: TNumberNumericalExpressionData =
-                    numberNumericalExpressionAnalyzer.analyze(number);
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
 
-                evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
 
-            it('should return correct number numerical expression data', () => {
-                assert.equal(number, evaluatedResult);
+            describe('Variant #4: positive unsafe big number', () => {
+                const number: number = Number.MAX_SAFE_INTEGER + 1;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
         });
 
-        describe('Variant #3: zero number', () => {
-            const number: number = 0;
+        describe('Negative numbers', () => {
+            describe('Variant #1: negative number', () => {
+                const number: number = -1234;
 
-            before(() => {
-                const numberNumericalExpressionData: TNumberNumericalExpressionData =
-                    numberNumericalExpressionAnalyzer.analyze(number);
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
 
-                evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
 
-            it('should return correct number numerical expression data', () => {
-                assert.equal(number, evaluatedResult);
+            describe('Variant #2: negative zero number', () => {
+                const number: number = -0;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #3: negative big number', () => {
+                const number: number = Number.MIN_SAFE_INTEGER;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #4: negative unsafe number', () => {
+                const number: number = Number.MIN_SAFE_INTEGER - 1;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
             });
         });
 
-        describe('Variant #4: NaN', () => {
+        describe('NaN', () => {
             const number: number = NaN;
 
             let testFunc: () => void;

+ 414 - 0
test/unit-tests/utils/NumberUtils.spec.ts

@@ -128,4 +128,418 @@ describe('NumberUtils', () => {
             });
         });
     });
+
+    describe('isPositive', () => {
+        describe('Variant #1: positive integer', () => {
+            const number: number = 2;
+            const expectedResult: boolean = true;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return true', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #2: positive zero', () => {
+            const number: number = 0;
+            const expectedResult: boolean = true;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return true', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #3: negative integer', () => {
+            const number: number = -2;
+            const expectedResult: boolean = false;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return false', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #4: negative zero', () => {
+            const number: number = -0;
+            const expectedResult: boolean = false;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return false', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #5: NaN', () => {
+            const number: number = NaN;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.isPositive(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, 'Given value is NaN');
+            });
+        });
+    });
+
+    describe('isUnsafeNumber', () => {
+        describe('Positive number', () => {
+            describe('Variant #1: positive small safe integer', () => {
+                const number: number = 100;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #2: positive big safe integer', () => {
+                const number: number = Number.MAX_SAFE_INTEGER;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #3: positive unsafe integer', () => {
+                const number: number = Number.MAX_SAFE_INTEGER + 1;
+                const expectedResult: boolean = true
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return true', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+        });
+
+        describe('Negative number', () => {
+            describe('Variant #1: negative small safe integer', () => {
+                const number: number = -100;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #2: negative big safe integer', () => {
+                const number: number = Number.MIN_SAFE_INTEGER;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #3: negative unsafe integer', () => {
+                const number: number = Number.MIN_SAFE_INTEGER - 1;
+                const expectedResult: boolean = true
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return true', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+        });
+
+        describe('NaN', () => {
+            const number: number = NaN;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.isUnsafeNumber(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, 'Given value is NaN');
+            });
+        });
+    });
+
+    describe('getFactors', () => {
+        describe('Positive numbers', () => {
+            describe('Variant #1: positive number `1`', () => {
+                const number: number = 1;
+                const expectedFactors: number[] = [
+                    -1,
+                    1
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #2: positive number `2`', () => {
+                const number: number = 2;
+                const expectedFactors: number[] = [
+                    -2,
+                    -1,
+                    1,
+                    2
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #3: positive number `100`', () => {
+                const number: number = 100;
+                const expectedFactors: number[] = [
+                    -100,
+                    -50,
+                    -25,
+                    -20,
+                    -10,
+                    -5,
+                    -4,
+                    -2,
+                    -1,
+                    1,
+                    2,
+                    4,
+                    5,
+                    10,
+                    20,
+                    25,
+                    50,
+                    100
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #4: positive number `9007199254740991`', () => {
+                const number: number = 9007199254740991;
+                const expectedFactors: number[] = [
+                    Number.MIN_SAFE_INTEGER,
+                    -1416003655831,
+                    -129728784761,
+                    -441650591,
+                    -20394401,
+                    -69431,
+                    -6361,
+                    -1,
+                    1,
+                    6361,
+                    69431,
+                    20394401,
+                    441650591,
+                    129728784761,
+                    1416003655831,
+                    Number.MAX_SAFE_INTEGER
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+        })
+
+        describe('Negative numbers', () => {
+            describe('Variant #1: negative number `-1`', () => {
+                const number: number = -1;
+                const expectedFactors: number[] = [
+                    -1,
+                    1
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #2: negative number `-2`', () => {
+                const number: number = -2;
+                const expectedFactors: number[] = [
+                    -2,
+                    -1,
+                    1,
+                    2
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #3: negative number `-100`', () => {
+                const number: number = -100;
+                const expectedFactors: number[] = [
+                    -100,
+                    -50,
+                    -25,
+                    -20,
+                    -10,
+                    -5,
+                    -4,
+                    -2,
+                    -1,
+                    1,
+                    2,
+                    4,
+                    5,
+                    10,
+                    20,
+                    25,
+                    50,
+                    100
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #4: negative number `-9007199254740991`', () => {
+                const number: number = -9007199254740991;
+                const expectedFactors: number[] = [
+                    Number.MIN_SAFE_INTEGER,
+                    -1416003655831,
+                    -129728784761,
+                    -441650591,
+                    -20394401,
+                    -69431,
+                    -6361,
+                    -1,
+                    1,
+                    6361,
+                    69431,
+                    20394401,
+                    441650591,
+                    129728784761,
+                    1416003655831,
+                    Number.MAX_SAFE_INTEGER
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+        })
+
+        describe('zero number', () => {
+            const number: number = 0;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.getFactors(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, Error);
+            });
+        })
+    });
 });

Some files were not shown because too many files changed in this diff