浏览代码

Merge pull request #493 from javascript-obfuscator/string-array-random-position

`shuffleStringArray` option
Timofey Kachalov 5 年之前
父节点
当前提交
c3b0b4f369
共有 63 个文件被更改,包括 1688 次插入330 次删除
  1. 2 0
      CHANGELOG.md
  2. 12 0
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 118 0
      src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts
  7. 5 1
      src/cli/JavaScriptObfuscatorCLI.ts
  8. 1 0
      src/container/ServiceIdentifiers.ts
  9. 7 0
      src/container/modules/analyzers/AnalyzersModule.ts
  10. 2 2
      src/container/modules/storages/StoragesModule.ts
  11. 6 25
      src/custom-nodes/string-array-nodes/StringArrayNode.ts
  12. 5 5
      src/custom-nodes/string-array-nodes/StringArrayRotateFunctionNode.ts
  13. 11 19
      src/custom-nodes/string-array-nodes/group/StringArrayCustomNodeGroup.ts
  14. 1 1
      src/interfaces/IEncodedValue.d.ts
  15. 17 0
      src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.d.ts
  16. 4 4
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IIdentifierObfuscatingReplacer.d.ts
  17. 6 2
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer.d.ts
  18. 0 4
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData.d.ts
  19. 1 0
      src/interfaces/options/IOptions.d.ts
  20. 7 1
      src/interfaces/storages/IArrayStorage.d.ts
  21. 7 1
      src/interfaces/storages/IMapStorage.d.ts
  22. 24 0
      src/interfaces/storages/string-array-storage/IStringArrayStorage.d.ts
  23. 6 0
      src/interfaces/storages/string-array-storage/IStringArrayStorageItem.d.ts
  24. 2 2
      src/node-transformers/obfuscating-transformers/CatchClauseTransformer.ts
  25. 4 4
      src/node-transformers/obfuscating-transformers/ClassDeclarationTransformer.ts
  26. 4 4
      src/node-transformers/obfuscating-transformers/FunctionDeclarationTransformer.ts
  27. 3 3
      src/node-transformers/obfuscating-transformers/FunctionTransformer.ts
  28. 3 3
      src/node-transformers/obfuscating-transformers/ImportDeclarationTransformer.ts
  29. 2 2
      src/node-transformers/obfuscating-transformers/LabeledStatementTransformer.ts
  30. 21 4
      src/node-transformers/obfuscating-transformers/LiteralTransformer.ts
  31. 4 4
      src/node-transformers/obfuscating-transformers/VariableDeclarationTransformer.ts
  32. 2 2
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/AbstractObfuscatingReplacer.ts
  33. 21 15
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.ts
  34. 9 3
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.ts
  35. 15 9
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.ts
  36. 55 147
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.ts
  37. 6 0
      src/options/Options.ts
  38. 1 0
      src/options/normalizer-rules/StringArrayRule.ts
  39. 1 0
      src/options/presets/Default.ts
  40. 1 0
      src/options/presets/NoCustomNodes.ts
  41. 10 2
      src/storages/ArrayStorage.ts
  42. 10 2
      src/storages/MapStorage.ts
  43. 215 15
      src/storages/string-array/StringArrayStorage.ts
  44. 1 1
      src/templates/string-array-nodes/string-array-rotate-function-node/StringArrayRotateFunctionTemplate.ts
  45. 0 3
      src/types/storages/TStringArrayStorage.d.ts
  46. 8 4
      test/dev/dev.ts
  47. 2 1
      test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts
  48. 58 15
      test/functional-tests/node-transformers/obfuscating-transformers/literal-transformer/LiteralTransformer.spec.ts
  49. 27 3
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.spec.ts
  50. 3 0
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/global-reserved-names.js
  51. 0 0
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/local-reserved-names.js
  52. 4 0
      test/functional-tests/options/OptionsNormalizer.spec.ts
  53. 308 0
      test/functional-tests/storages/string-array-storage/StringArrayStorage.spec.ts
  54. 1 0
      test/functional-tests/storages/string-array-storage/fixtures/one-string.js
  55. 3 0
      test/functional-tests/storages/string-array-storage/fixtures/three-strings.js
  56. 1 1
      test/helpers/getRegExpMatch.ts
  57. 5 0
      test/index.spec.ts
  58. 318 0
      test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts
  59. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec.ts
  60. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec.ts
  61. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec.ts
  62. 101 14
      test/unit-tests/storages/ArrayStorage.spec.ts
  63. 37 2
      test/unit-tests/storages/MapStorage.spec.ts

+ 2 - 0
CHANGELOG.md

@@ -2,8 +2,10 @@ Change Log
 
 v0.23.0
 ---
+* **New option:** `shuffleStringArray` randomly shuffles string array items
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/494
 * **Internal change:** switched AST parser from `espree` on `acorn`
+* **Internal refactoring:** refactoring of string array storage and related things
 
 v0.22.1
 ---

+ 12 - 0
README.md

@@ -306,6 +306,7 @@ Following options are available for the JS Obfuscator:
     rotateStringArray: true,
     seed: 0,
     selfDefending: false,
+    shuffleStringArray: true,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
@@ -349,6 +350,7 @@ Following options are available for the JS Obfuscator:
     --rotate-string-array <boolean>
     --seed <string|number>
     --self-defending <boolean>
+    --shuffle-string-array <boolean>
     --source-map <boolean>
     --source-map-base-url <string>
     --source-map-file-name <string>
@@ -691,6 +693,13 @@ Type: `boolean` Default: `false`
 
 This option makes the output code resilient against formatting and variable renaming. If one tries to use a JavaScript beautifier on the obfuscated code, the code won't work anymore, making it harder to understand and modify it.
 
+### `shuffleStringArray`
+Type: `boolean` Default: `true`
+
+##### :warning: [`stringArray`](#stringarray) must be enabled
+
+Randomly shuffles the `stringArray` array items.
+
 ### `sourceMap`
 Type: `boolean` Default: `false`
 
@@ -867,6 +876,7 @@ Performance will 50-100% slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: true,
     splitStringsChunkLength: '5',
     stringArray: true,
@@ -896,6 +906,7 @@ Performance will 30-35% slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: true,
     splitStringsChunkLength: '10',
     stringArray: true,
@@ -923,6 +934,7 @@ Performance will slightly slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: false,
     stringArray: true,
     stringArrayEncoding: false,

文件差异内容过多而无法显示
+ 0 - 0
dist/index.browser.js


文件差异内容过多而无法显示
+ 0 - 0
dist/index.cli.js


文件差异内容过多而无法显示
+ 0 - 0
dist/index.js


+ 118 - 0
src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts

@@ -0,0 +1,118 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as estraverse from 'estraverse';
+import * as ESTree from 'estree';
+
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { NodeGuards } from '../../node/NodeGuards';
+import { NodeMetadata } from '../../node/NodeMetadata';
+
+/**
+ * Adds values of literal nodes to the string array storage
+ */
+@injectable()
+export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer {
+    /**
+     * @type {number}
+     */
+    private static readonly minimumLengthForStringArray: number = 3;
+
+    /**
+     * @type {IOptions}
+     */
+    private readonly options: IOptions;
+
+    /**
+     * @type {randomGenerator}
+     */
+    private readonly randomGenerator: IRandomGenerator;
+
+    /**
+     * @type {IStringArrayStorage}
+     */
+    private readonly stringArrayStorage: IStringArrayStorage;
+
+    /**
+     * @type {Map<ESTree.Literal, IStringArrayStorageItemData>}
+     */
+    private readonly stringArrayStorageData: Map<ESTree.Literal, IStringArrayStorageItemData> = new Map();
+
+    /**
+     * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    constructor (
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+    ) {
+        this.stringArrayStorage = stringArrayStorage;
+        this.randomGenerator = randomGenerator;
+        this.options = options;
+    }
+
+    /**
+     * @param {Program} astTree
+     */
+    public analyze (astTree: ESTree.Program): void {
+        if (!this.options.stringArray) {
+            return;
+        }
+
+        estraverse.traverse(astTree, {
+            enter: (node: ESTree.Node): estraverse.VisitorOption | void => {
+                if (NodeMetadata.isIgnoredNode(node)) {
+                    return estraverse.VisitorOption.Skip;
+                }
+
+                if (!NodeGuards.isLiteralNode(node)) {
+                    return;
+                }
+
+                this.analyzeLiteralNode(node);
+            }
+        });
+    }
+
+    /**
+     * @param {Literal} literalNode
+     * @returns {IStringArrayStorageItemData | undefined}
+     */
+    public getItemDataForLiteralNode (literalNode: ESTree.Literal): IStringArrayStorageItemData | undefined {
+        return this.stringArrayStorageData.get(literalNode);
+    }
+
+    /**
+     * @param {Literal} literalNode
+     */
+    private analyzeLiteralNode (literalNode: ESTree.Literal): void {
+        if (typeof literalNode.value !== 'string') {
+            return;
+        }
+
+        if (!this.shouldAddValueToStringArray(literalNode.value)) {
+            return;
+        }
+
+        this.stringArrayStorageData.set(
+            literalNode,
+            this.stringArrayStorage.getOrThrow(literalNode.value)
+        );
+    }
+
+    /**
+     * @param {string} value
+     * @returns {boolean}
+     */
+    private shouldAddValueToStringArray (value: string): boolean {
+        return value.length >= StringArrayStorageAnalyzer.minimumLengthForStringArray
+            && this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold;
+    }
+}

+ 5 - 1
src/cli/JavaScriptObfuscatorCLI.ts

@@ -283,7 +283,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 BooleanSanitizer
             )
             .option(
-                '--rotate-string-array <boolean>', 'Disable rotation of unicode array values during obfuscation',
+                '--rotate-string-array <boolean>', 'Enable rotation of string array values during obfuscation',
                 BooleanSanitizer
             )
             .option(
@@ -296,6 +296,10 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 'Disables self-defending for obfuscated code',
                 BooleanSanitizer
             )
+            .option(
+                '--shuffle-string-array <boolean>', 'Randomly shuffles string array items',
+                BooleanSanitizer
+            )
             .option(
                 '--source-map <boolean>',
                 'Enables source map generation',

+ 1 - 0
src/container/ServiceIdentifiers.ts

@@ -41,6 +41,7 @@ export enum ServiceIdentifiers {
     IRandomGenerator = 'IRandomGenerator',
     ISourceCode = 'ISourceCode',
     ISourceMapCorrector = 'ISourceMapCorrector',
+    IStringArrayStorageAnalyzer = 'IStringArrayStorageAnalyzer',
     ITransformersRunner = 'ITransformersRunner',
     Newable__ICustomNode = 'Newable<ICustomNode>',
     Newable__TControlFlowStorage = 'Newable<TControlFlowStorage>',

+ 7 - 0
src/container/modules/analyzers/AnalyzersModule.ts

@@ -5,6 +5,7 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 import { ICalleeDataExtractor } from '../../../interfaces/analyzers/calls-graph-analyzer/ICalleeDataExtractor';
 import { ICallsGraphAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphAnalyzer';
 import { IPrevailingKindOfVariablesAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/IPrevailingKindOfVariablesAnalyzer';
+import { IStringArrayStorageAnalyzer } from '../../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 
 import { CalleeDataExtractor } from '../../../enums/analyzers/calls-graph-analyzer/CalleeDataExtractor';
 import { CallsGraphAnalyzer } from '../../../analyzers/calls-graph-analyzer/CallsGraphAnalyzer';
@@ -12,6 +13,7 @@ import { FunctionDeclarationCalleeDataExtractor } from '../../../analyzers/calls
 import { FunctionExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/FunctionExpressionCalleeDataExtractor';
 import { ObjectExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/ObjectExpressionCalleeDataExtractor';
 import { PrevailingKindOfVariablesAnalyzer } from '../../../analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer';
+import { StringArrayStorageAnalyzer } from '../../../analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer';
 
 export const analyzersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // calls graph analyzer
@@ -24,6 +26,11 @@ export const analyzersModule: interfaces.ContainerModule = new ContainerModule((
         .to(PrevailingKindOfVariablesAnalyzer)
         .inSingletonScope();
 
+    // string array storage analyzer
+    bind<IStringArrayStorageAnalyzer>(ServiceIdentifiers.IStringArrayStorageAnalyzer)
+        .to(StringArrayStorageAnalyzer)
+        .inSingletonScope();
+
     // callee data extractors
     bind<ICalleeDataExtractor>(ServiceIdentifiers.ICalleeDataExtractor)
         .to(FunctionDeclarationCalleeDataExtractor)

+ 2 - 2
src/container/modules/storages/StoragesModule.ts

@@ -3,10 +3,10 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 
 import { TControlFlowStorage } from '../../../types/storages/TControlFlowStorage';
 import { TCustomNodeGroupStorage } from '../../../types/storages/TCustomNodeGroupStorage';
-import { TStringArrayStorage } from '../../../types/storages/TStringArrayStorage';
 
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 import { ControlFlowStorage } from '../../../storages/control-flow/ControlFlowStorage';
 import { CustomNodeGroupStorage } from '../../../storages/custom-node-group/CustomNodeGroupStorage';
@@ -18,7 +18,7 @@ export const storagesModule: interfaces.ContainerModule = new ContainerModule((b
         .to(CustomNodeGroupStorage)
         .inSingletonScope();
 
-    bind<TStringArrayStorage>(ServiceIdentifiers.TStringArrayStorage)
+    bind<IStringArrayStorage>(ServiceIdentifiers.TStringArrayStorage)
         .to(StringArrayStorage)
         .inSingletonScope();
 

+ 6 - 25
src/custom-nodes/string-array-nodes/StringArrayNode.ts

@@ -3,11 +3,11 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TStatement } from '../../types/node/TStatement';
-import { TStringArrayStorage } from '../../types/storages/TStringArrayStorage';
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { ICustomNodeFormatter } from '../../interfaces/custom-nodes/ICustomNodeFormatter';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 import { initializable } from '../../decorators/Initializable';
 
@@ -15,15 +15,14 @@ import { StringArrayTemplate } from '../../templates/string-array-nodes/string-a
 
 import { AbstractCustomNode } from '../AbstractCustomNode';
 import { NodeUtils } from '../../node/NodeUtils';
-import { StringArrayStorage } from '../../storages/string-array/StringArrayStorage';
 
 @injectable()
 export class StringArrayNode extends AbstractCustomNode {
     /**
-     * @type {TStringArrayStorage}
+     * @type {IStringArrayStorage}
      */
     @initializable()
-    private stringArrayStorage!: TStringArrayStorage;
+    private stringArrayStorage!: IStringArrayStorage;
 
     /**
      * @type {string}
@@ -31,12 +30,6 @@ export class StringArrayNode extends AbstractCustomNode {
     @initializable()
     private stringArrayName!: string;
 
-    /**
-     * @type {number}
-     */
-    @initializable()
-    private stringArrayRotateValue!: number;
-
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {ICustomNodeFormatter} customNodeFormatter
@@ -54,27 +47,15 @@ export class StringArrayNode extends AbstractCustomNode {
     }
 
     /**
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
      * @param {string} stringArrayName
-     * @param {number} stringArrayRotateValue
      */
     public initialize (
-        stringArrayStorage: TStringArrayStorage,
-        stringArrayName: string,
-        stringArrayRotateValue: number
+        stringArrayStorage: IStringArrayStorage,
+        stringArrayName: string
     ): void {
         this.stringArrayStorage = stringArrayStorage;
         this.stringArrayName = stringArrayName;
-        this.stringArrayRotateValue = stringArrayRotateValue;
-    }
-
-    /**
-     * @returns {TStatement[]}
-     */
-    public getNode (): TStatement[] {
-        (<StringArrayStorage>this.stringArrayStorage).rotateArray(this.stringArrayRotateValue);
-
-        return super.getNode();
     }
 
     /**

+ 5 - 5
src/custom-nodes/string-array-nodes/StringArrayRotateFunctionNode.ts

@@ -38,7 +38,7 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
      * @param {number}
      */
     @initializable()
-    private stringArrayRotateValue!: number;
+    private stringArrayRotationAmount!: number;
 
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
@@ -62,14 +62,14 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
 
     /**
      * @param {string} stringArrayName
-     * @param {number} stringArrayRotateValue
+     * @param {number} stringArrayRotationAmount
      */
     public initialize (
         stringArrayName: string,
-        stringArrayRotateValue: number
+        stringArrayRotationAmount: number
     ): void {
         this.stringArrayName = stringArrayName;
-        this.stringArrayRotateValue = stringArrayRotateValue;
+        this.stringArrayRotationAmount = stringArrayRotationAmount;
     }
 
     /**
@@ -103,7 +103,7 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
                 code,
                 timesName,
                 stringArrayName: this.stringArrayName,
-                stringArrayRotateValue: NumberUtils.toHex(this.stringArrayRotateValue),
+                stringArrayRotationAmount: NumberUtils.toHex(this.stringArrayRotationAmount),
                 whileFunctionName
             }),
             {

+ 11 - 19
src/custom-nodes/string-array-nodes/group/StringArrayCustomNodeGroup.ts

@@ -5,12 +5,12 @@ import { TCustomNodeFactory } from '../../../types/container/custom-nodes/TCusto
 import { TIdentifierNamesGeneratorFactory } from '../../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TInitialData } from '../../../types/TInitialData';
 import { TNodeWithStatements } from '../../../types/node/TNodeWithStatements';
-import { TStringArrayStorage } from '../../../types/storages/TStringArrayStorage';
 
+import { ICallsGraphData } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphData';
 import { ICustomNode } from '../../../interfaces/custom-nodes/ICustomNode';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
-import { ICallsGraphData } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphData';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 import { initializable } from '../../../decorators/Initializable';
 
@@ -42,20 +42,20 @@ export class StringArrayCustomNodeGroup extends AbstractCustomNodeGroup {
     private readonly customNodeFactory: TCustomNodeFactory;
 
     /**
-     * @type {TStringArrayStorage}
+     * @type {IStringArrayStorage}
      */
-    private readonly stringArrayStorage: TStringArrayStorage;
+    private readonly stringArrayStorage: IStringArrayStorage;
 
     /**
      * @param {TCustomNodeFactory} customNodeFactory
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
      */
     constructor (
         @inject(ServiceIdentifiers.Factory__ICustomNode) customNodeFactory: TCustomNodeFactory,
-        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: TStringArrayStorage,
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
@@ -106,21 +106,13 @@ export class StringArrayCustomNodeGroup extends AbstractCustomNodeGroup {
         const stringArrayRotateFunctionNode: ICustomNode<TInitialData<StringArrayRotateFunctionNode>> =
             this.customNodeFactory(CustomNode.StringArrayRotateFunctionNode);
 
-        const stringArrayStorageId: string = this.stringArrayStorage.getStorageId();
-
-        const [stringArrayName, stringArrayCallsWrapperName]: string[] = stringArrayStorageId.split('|');
-
-        let stringArrayRotateValue: number;
-
-        if (this.options.rotateStringArray) {
-            stringArrayRotateValue = this.randomGenerator.getRandomInteger(100, 500);
-        } else {
-            stringArrayRotateValue = 0;
-        }
+        const stringArrayName: string = this.stringArrayStorage.getStorageName();
+        const stringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName();
+        const stringArrayRotationAmount: number = this.stringArrayStorage.getRotationAmount();
 
-        stringArrayNode.initialize(this.stringArrayStorage, stringArrayName, stringArrayRotateValue);
+        stringArrayNode.initialize(this.stringArrayStorage, stringArrayName);
         stringArrayCallsWrapper.initialize(stringArrayName, stringArrayCallsWrapperName);
-        stringArrayRotateFunctionNode.initialize(stringArrayName, stringArrayRotateValue);
+        stringArrayRotateFunctionNode.initialize(stringArrayName, stringArrayRotationAmount);
 
         this.customNodes.set(CustomNode.StringArrayNode, stringArrayNode);
         this.customNodes.set(CustomNode.StringArrayCallsWrapper, stringArrayCallsWrapper);

+ 1 - 1
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IEncodedValue.d.ts → src/interfaces/IEncodedValue.d.ts

@@ -1,4 +1,4 @@
 export interface IEncodedValue {
     encodedValue: string;
-    key: string | null;
+    decodeKey: string | null;
 }

+ 17 - 0
src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.d.ts

@@ -0,0 +1,17 @@
+import * as ESTree from 'estree';
+
+import { IAnalyzer } from '../IAnalyzer';
+import { IStringArrayStorageItemData } from '../../storages/string-array-storage/IStringArrayStorageItem';
+
+export interface IStringArrayStorageAnalyzer extends IAnalyzer<void> {
+    /**
+     * @param {Program} astTree
+     */
+    analyze (astTree: ESTree.Program): void;
+
+    /**
+     * @param {Literal} literalNode
+     * @returns {IStringArrayStorageItemData | undefined}
+     */
+    getItemDataForLiteralNode (literalNode: ESTree.Literal): IStringArrayStorageItemData | undefined;
+}

+ 4 - 4
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IIdentifierObfuscatingReplacer.d.ts

@@ -6,16 +6,16 @@ import { IObfuscatingReplacer } from './IObfuscatingReplacer';
 
 export interface IIdentifierObfuscatingReplacer extends IObfuscatingReplacer <ESTree.Identifier> {
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
-    storeGlobalName (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): void;
+    storeGlobalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
 
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
-    storeLocalName (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): void;
+    storeLocalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
 
     /**
      * @param {string} name

+ 6 - 2
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer.d.ts

@@ -4,10 +4,14 @@ import { TNodeWithLexicalScope } from '../../../../types/node/TNodeWithLexicalSc
 
 export interface IObfuscatingReplacer <T = ESTree.Node> {
     /**
-     * @param {SimpleLiteral["value"]} nodeValue
+     * @param {Node} node
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {number} nodeIdentifier
      * @returns {T}
      */
-    replace (nodeValue: ESTree.SimpleLiteral['value'], lexicalScopeNode?: TNodeWithLexicalScope, nodeIdentifier?: number): T;
+    replace (
+        node: ESTree.Node,
+        lexicalScopeNode?: TNodeWithLexicalScope,
+        nodeIdentifier?: number
+    ): T;
 }

+ 0 - 4
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData.d.ts

@@ -1,4 +0,0 @@
-export interface IStringArrayIndexData {
-    fromCache: boolean;
-    index: string;
-}

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

@@ -27,6 +27,7 @@ export interface IOptions {
     readonly rotateStringArray: boolean;
     readonly seed: string | number;
     readonly selfDefending: boolean;
+    readonly shuffleStringArray: boolean;
     readonly sourceMap: boolean;
     readonly sourceMapBaseUrl: string;
     readonly sourceMapFileName: string;

+ 7 - 1
src/interfaces/storages/IArrayStorage.d.ts

@@ -1,11 +1,17 @@
 import { IInitializable } from '../IInitializable';
 
 export interface IArrayStorage <V> extends IInitializable {
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    get (key: number): V | undefined;
+
     /**
      * @param {number} key
      * @returns {V}
      */
-    get (key: number): V;
+    getOrThrow (key: number): V;
 
     /**
      * @param value

+ 7 - 1
src/interfaces/storages/IMapStorage.d.ts

@@ -1,11 +1,17 @@
 import { IInitializable } from '../IInitializable';
 
 export interface IMapStorage <K, V> extends IInitializable {
+    /**
+     * @param {K} key
+     * @returns {V | undefined}
+     */
+    get (key: K): V | undefined;
+
     /**
      * @param {K} key
      * @returns {V}
      */
-    get (key: K): V;
+    getOrThrow (key: K): V;
 
     /**
      * @param {V} value

+ 24 - 0
src/interfaces/storages/string-array-storage/IStringArrayStorage.d.ts

@@ -0,0 +1,24 @@
+import { IStringArrayStorageItemData } from './IStringArrayStorageItem';
+
+import { IMapStorage } from '../IMapStorage';
+
+export interface IStringArrayStorage extends IMapStorage <string, IStringArrayStorageItemData> {
+    /**
+     * @returns {number}
+     */
+    getRotationAmount (): number;
+
+    /**
+     * @returns {string}
+     */
+    getStorageName (): string;
+
+    /**
+     * @returns {string}
+     */
+    getStorageCallsWrapperName (): string;
+
+    rotateStorage (): void;
+
+    shuffleStorage (): void;
+}

+ 6 - 0
src/interfaces/storages/string-array-storage/IStringArrayStorageItem.d.ts

@@ -0,0 +1,6 @@
+import { IEncodedValue } from '../../IEncodedValue';
+
+export interface IStringArrayStorageItemData extends IEncodedValue {
+    index: number;
+    value: string;
+}

+ 2 - 2
src/node-transformers/obfuscating-transformers/CatchClauseTransformer.ts

@@ -100,7 +100,7 @@ export class CatchClauseTransformer extends AbstractNodeTransformer {
         lexicalScopeNode: TNodeWithLexicalScope
     ): void {
         if (catchClauseNode.param && NodeGuards.isIdentifierNode(catchClauseNode.param)) {
-            this.identifierObfuscatingReplacer.storeLocalName(catchClauseNode.param.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(catchClauseNode.param, lexicalScopeNode);
         }
     }
 
@@ -116,7 +116,7 @@ export class CatchClauseTransformer extends AbstractNodeTransformer {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
                 if (parentNode && NodeGuards.isReplaceableIdentifierNode(node, parentNode)) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 4 - 4
src/node-transformers/obfuscating-transformers/ClassDeclarationTransformer.ts

@@ -130,9 +130,9 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
         isGlobalDeclaration: boolean
     ): void {
         if (isGlobalDeclaration) {
-            this.identifierObfuscatingReplacer.storeGlobalName(classDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(classDeclarationNode.id, lexicalScopeNode);
         } else {
-            this.identifierObfuscatingReplacer.storeLocalName(classDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(classDeclarationNode.id, lexicalScopeNode);
         }
     }
 
@@ -159,7 +159,7 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -180,7 +180,7 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 4 - 4
src/node-transformers/obfuscating-transformers/FunctionDeclarationTransformer.ts

@@ -130,9 +130,9 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
         isGlobalDeclaration: boolean
     ): void {
         if (isGlobalDeclaration) {
-            this.identifierObfuscatingReplacer.storeGlobalName(functionDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(functionDeclarationNode.id, lexicalScopeNode);
         } else {
-            this.identifierObfuscatingReplacer.storeLocalName(functionDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(functionDeclarationNode.id, lexicalScopeNode);
         }
     }
 
@@ -159,7 +159,7 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -181,7 +181,7 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 3 - 3
src/node-transformers/obfuscating-transformers/FunctionTransformer.ts

@@ -154,13 +154,13 @@ export class FunctionTransformer extends AbstractNodeTransformer {
                 }
 
                 if (NodeGuards.isAssignmentPatternNode(node) && NodeGuards.isIdentifierNode(node.left)) {
-                    this.identifierObfuscatingReplacer.storeLocalName(node.left.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(node.left, lexicalScopeNode);
 
                     return estraverse.VisitorOption.Skip;
                 }
 
                 if (NodeGuards.isIdentifierNode(node)) {
-                    this.identifierObfuscatingReplacer.storeLocalName(node.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(node, lexicalScopeNode);
                 }
             }
         };
@@ -212,7 +212,7 @@ export class FunctionTransformer extends AbstractNodeTransformer {
                     }
 
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 3 - 3
src/node-transformers/obfuscating-transformers/ImportDeclarationTransformer.ts

@@ -126,7 +126,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
                 return;
             }
 
-            this.identifierObfuscatingReplacer.storeGlobalName(importSpecifierNode.local.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(importSpecifierNode.local, lexicalScopeNode);
         });
     }
 
@@ -139,7 +139,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
 
         cachedReplaceableIdentifiers.forEach((replaceableIdentifier: ESTree.Identifier) => {
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
             if (replaceableIdentifier.name !== newReplaceableIdentifier.name) {
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
@@ -162,7 +162,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 2 - 2
src/node-transformers/obfuscating-transformers/LabeledStatementTransformer.ts

@@ -106,7 +106,7 @@ export class LabeledStatementTransformer extends AbstractNodeTransformer {
         labeledStatementNode: ESTree.LabeledStatement,
         lexicalScopeNode: TNodeWithLexicalScope
     ): void {
-        this.identifierObfuscatingReplacer.storeLocalName(labeledStatementNode.label.name, lexicalScopeNode);
+        this.identifierObfuscatingReplacer.storeLocalName(labeledStatementNode.label, lexicalScopeNode);
     }
 
     /**
@@ -121,7 +121,7 @@ export class LabeledStatementTransformer extends AbstractNodeTransformer {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
                 if (parentNode && NodeGuards.isLabelIdentifierNode(node, parentNode)) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
 
                     node.name = newIdentifier.name;
                 }

+ 21 - 4
src/node-transformers/obfuscating-transformers/LiteralTransformer.ts

@@ -7,6 +7,7 @@ import { TLiteralObfuscatingReplacerFactory } from '../../types/container/node-t
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 
 import { LiteralObfuscatingReplacer } from '../../enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
@@ -24,20 +25,28 @@ export class LiteralTransformer extends AbstractNodeTransformer {
      */
     private readonly literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory;
 
+    /**
+     * @type {IStringArrayStorageAnalyzer}
+     */
+    private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
+
     /**
      * @param {TLiteralObfuscatingReplacerFactory} literalObfuscatingReplacerFactory
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
+     * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      */
     constructor (
         @inject(ServiceIdentifiers.Factory__IObfuscatingReplacer)
             literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.IOptions) options: IOptions
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer
     ) {
         super(randomGenerator, options);
 
         this.literalObfuscatingReplacerFactory = literalObfuscatingReplacerFactory;
+        this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
     }
 
     /**
@@ -49,6 +58,10 @@ export class LiteralTransformer extends AbstractNodeTransformer {
             case TransformationStage.Obfuscating:
                 return {
                     enter: (node: ESTree.Node, parentNode: ESTree.Node | null) => {
+                        if (NodeGuards.isProgramNode(node)) {
+                            this.analyzeNode(node);
+                        }
+
                         if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
                             return this.transformNode(node, parentNode);
                         }
@@ -60,6 +73,10 @@ export class LiteralTransformer extends AbstractNodeTransformer {
         }
     }
 
+    public analyzeNode (programNode: ESTree.Program): void {
+        this.stringArrayStorageAnalyzer.analyze(programNode);
+    }
+
     /**
      * @param {Literal} literalNode
      * @param {NodeGuards} parentNode
@@ -76,21 +93,21 @@ export class LiteralTransformer extends AbstractNodeTransformer {
             case 'boolean':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.BooleanLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
                 break;
 
             case 'number':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.NumberLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
                 break;
 
             case 'string':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.StringLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
                 break;
 

+ 4 - 4
src/node-transformers/obfuscating-transformers/VariableDeclarationTransformer.ts

@@ -153,9 +153,9 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                 }
 
                 if (isGlobalDeclaration) {
-                    this.identifierObfuscatingReplacer.storeGlobalName(identifierNode.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeGlobalName(identifierNode, lexicalScopeNode);
                 } else {
-                    this.identifierObfuscatingReplacer.storeLocalName(identifierNode.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(identifierNode, lexicalScopeNode);
                 }
             }
         );
@@ -287,7 +287,7 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                 }
 
                 const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                    .replace(replaceableIdentifier.name, lexicalScopeNode);
+                    .replace(replaceableIdentifier, lexicalScopeNode);
 
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
                 NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -310,7 +310,7 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
 
                     if (node.name !== newIdentifierName) {

+ 2 - 2
src/node-transformers/obfuscating-transformers/obfuscating-replacers/AbstractObfuscatingReplacer.ts

@@ -25,9 +25,9 @@ export abstract class AbstractObfuscatingReplacer implements IObfuscatingReplace
     }
 
     /**
-     * @param {SimpleLiteral["value"]} nodeValue
+     * @param {Node} node
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @returns {Node}
      */
-    public abstract replace (nodeValue: ESTree.SimpleLiteral['value'], lexicalScopeNode?: TNodeWithLexicalScope): ESTree.Node;
+    public abstract replace (node: ESTree.Node, lexicalScopeNode?: TNodeWithLexicalScope): ESTree.Node;
 }

+ 21 - 15
src/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.ts

@@ -40,35 +40,39 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
     }
 
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @returns {Identifier}
      */
-    public replace (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): ESTree.Identifier {
+    public replace (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): ESTree.Identifier {
+        let identifierName: string = identifierNode.name;
+
         if (this.blockScopesMap.has(lexicalScopeNode)) {
             const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
-            if (namesMap.has(nodeValue)) {
-                nodeValue = <string>namesMap.get(nodeValue);
+            if (namesMap.has(identifierName)) {
+                identifierName = <string>namesMap.get(identifierName);
             }
         }
 
-        return NodeFactory.identifierNode(nodeValue);
+        return NodeFactory.identifierNode(identifierName);
     }
 
     /**
      * Store `nodeName` of global identifiers as key in map with random name as value.
      * Reserved name will be ignored.
      *
-     * @param {string} nodeName
+     * @param {Node} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
-    public storeGlobalName (nodeName: string, lexicalScopeNode: TNodeWithLexicalScope): void {
-        if (this.isReservedName(nodeName)) {
+    public storeGlobalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void {
+        const identifierName: string = identifierNode.name;
+
+        if (this.isReservedName(identifierName)) {
             return;
         }
 
-        const identifierName: string = this.identifierNamesGenerator.generateWithPrefix();
+        const newIdentifierName: string = this.identifierNamesGenerator.generateWithPrefix();
 
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
             this.blockScopesMap.set(lexicalScopeNode, new Map());
@@ -76,22 +80,24 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
 
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
-        namesMap.set(nodeName, identifierName);
+        namesMap.set(identifierName, newIdentifierName);
     }
 
     /**
      * Store `nodeName` of local identifier as key in map with random name as value.
      * Reserved name will be ignored.
      *
-     * @param {string} nodeName
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
-    public storeLocalName (nodeName: string, lexicalScopeNode: TNodeWithLexicalScope): void {
-        if (this.isReservedName(nodeName)) {
+    public storeLocalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void {
+        const identifierName: string = identifierNode.name;
+
+        if (this.isReservedName(identifierName)) {
             return;
         }
 
-        const identifierName: string = this.identifierNamesGenerator.generate();
+        const newIdentifierName: string = this.identifierNamesGenerator.generate();
 
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
             this.blockScopesMap.set(lexicalScopeNode, new Map());
@@ -99,7 +105,7 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
 
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
-        namesMap.set(nodeName, identifierName);
+        namesMap.set(identifierName, newIdentifierName);
     }
 
     /**

+ 9 - 3
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.ts

@@ -40,11 +40,17 @@ export class BooleanLiteralObfuscatingReplacer extends AbstractObfuscatingReplac
     }
 
     /**
-     * @param {boolean} nodeValue
+     * @param {SimpleLiteral} literalNode
      * @returns {Node}
      */
-    public replace (nodeValue: boolean): ESTree.Node {
-        return nodeValue
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
+
+        if (typeof literalValue !== 'boolean') {
+            throw new Error('`BooleanLiteralObfuscatingReplacer` should accept only literals with `boolean` value');
+        }
+
+        return literalValue
             ? BooleanLiteralObfuscatingReplacer.getTrueUnaryExpressionNode()
             : BooleanLiteralObfuscatingReplacer.getFalseUnaryExpressionNode();
     }

+ 15 - 9
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.ts

@@ -27,24 +27,30 @@ export class NumberLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     }
 
     /**
-     * @param {number} nodeValue
+     * @param {SimpleLiteral} literalNode
      * @returns {Node}
      */
-    public replace (nodeValue: number): ESTree.Node {
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
+
+        if (typeof literalValue !== 'number') {
+            throw new Error('`NumberLiteralObfuscatingReplacer` should accept only literals with `number` value');
+        }
+
         let rawValue: string;
 
-        if (this.numberLiteralCache.has(nodeValue)) {
-            rawValue = <string>this.numberLiteralCache.get(nodeValue);
+        if (this.numberLiteralCache.has(literalValue)) {
+            rawValue = <string>this.numberLiteralCache.get(literalValue);
         } else {
-            if (!NumberUtils.isCeil(nodeValue)) {
-                rawValue = String(nodeValue);
+            if (!NumberUtils.isCeil(literalValue)) {
+                rawValue = String(literalValue);
             } else {
-                rawValue = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(nodeValue)}`;
+                rawValue = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(literalValue)}`;
             }
 
-            this.numberLiteralCache.set(nodeValue, rawValue);
+            this.numberLiteralCache.set(literalValue, rawValue);
         }
 
-        return NodeFactory.literalNode(nodeValue, rawValue);
+        return NodeFactory.literalNode(literalValue, rawValue);
     }
 }

+ 55 - 147
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.ts

@@ -1,16 +1,16 @@
-import { inject, injectable, } from 'inversify';
+import { inject, injectable, postConstruct } from 'inversify';
 import { ServiceIdentifiers } from '../../../../container/ServiceIdentifiers';
 
 import * as ESTree from 'estree';
 
-import { TStringArrayStorage } from '../../../../types/storages/TStringArrayStorage';
-
-import { ICryptUtils } from '../../../../interfaces/utils/ICryptUtils';
-import { IEncodedValue } from '../../../../interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IEncodedValue';
 import { IEscapeSequenceEncoder } from '../../../../interfaces/utils/IEscapeSequenceEncoder';
+import { IInitializable } from '../../../../interfaces/IInitializable';
 import { IOptions } from '../../../../interfaces/options/IOptions';
-import { IRandomGenerator } from '../../../../interfaces/utils/IRandomGenerator';
-import { IStringArrayIndexData } from '../../../../interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData';
+import { IStringArrayStorage } from '../../../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorageAnalyzer } from '../../../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { initializable } from '../../../../decorators/Initializable';
 
 import { StringArrayEncoding } from '../../../../enums/StringArrayEncoding';
 
@@ -21,27 +21,7 @@ import { NumberUtils } from '../../../../utils/NumberUtils';
 import { Utils } from '../../../../utils/Utils';
 
 @injectable()
-export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplacer {
-    /**
-     * @type {number}
-     */
-    private static readonly minimumLengthForStringArray: number = 3;
-
-    /**
-     * @type {number}
-     */
-    private static readonly rc4KeyLength: number = 4;
-
-    /**
-     * @type {number}
-     */
-    private static readonly rc4KeysCount: number = 50;
-
-    /**
-     * @type {ICryptUtils}
-     */
-    private readonly cryptUtils: ICryptUtils;
-
+export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplacer implements IInitializable {
     /**
      * @type {IEscapeSequenceEncoder}
      */
@@ -53,55 +33,38 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     private readonly nodesCache: Map <string, ESTree.Node> = new Map();
 
     /**
-     * @type {IRandomGenerator}
+     * @type {IStringArrayStorage}
      */
-    private readonly randomGenerator: IRandomGenerator;
+    private readonly stringArrayStorage: IStringArrayStorage;
 
     /**
-     * @type {string[]}
+     * @type {IStringArrayStorageAnalyzer}
      */
-    private readonly rc4Keys: string[];
+    private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
 
     /**
-     * @type {Map<string, string>}
+     * @type {string}
      */
-    private readonly stringLiteralHexadecimalIndexCache: Map <string, string> = new Map();
+    @initializable()
+    private stringArrayStorageCallsWrapperName!: string;
 
     /**
-     * @type {TStringArrayStorage}
-     */
-    private readonly stringArrayStorage: TStringArrayStorage;
-
-    /**
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
-     * @param {IRandomGenerator} randomGenerator
-     * @param {ICryptUtils} cryptUtils
      * @param {IOptions} options
      */
     constructor (
-        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: TStringArrayStorage,
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer,
         @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder,
-        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.ICryptUtils) cryptUtils: ICryptUtils,
         @inject(ServiceIdentifiers.IOptions) options: IOptions
     ) {
-        super(
-            options
-        );
+        super(options);
 
         this.stringArrayStorage = stringArrayStorage;
+        this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
         this.escapeSequenceEncoder = escapeSequenceEncoder;
-        this.randomGenerator = randomGenerator;
-        this.cryptUtils = cryptUtils;
-
-        this.rc4Keys = this.randomGenerator.getRandomGenerator()
-            .n(
-                () => this.randomGenerator.getRandomGenerator().string({
-                    length: StringLiteralObfuscatingReplacer.rc4KeyLength
-                }),
-                StringLiteralObfuscatingReplacer.rc4KeysCount
-            );
     }
 
     /**
@@ -128,89 +91,46 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
         return rc4KeyLiteralNode;
     }
 
-    /**
-     * @param {string} nodeValue
-     * @returns {Node}
-     */
-    public replace (nodeValue: string): ESTree.Node {
-        const useStringArray: boolean = this.canUseStringArray(nodeValue);
-        const cacheKey: string = `${nodeValue}-${String(useStringArray)}`;
-        const useCacheValue: boolean = this.nodesCache.has(cacheKey) && this.options.stringArrayEncoding !== StringArrayEncoding.Rc4;
+    @postConstruct()
+    public initialize (): void {
+        this.stringArrayStorageCallsWrapperName = this.stringArrayStorage.getStorageCallsWrapperName();
 
-        if (useCacheValue) {
-            return <ESTree.Node>this.nodesCache.get(cacheKey);
+        if (this.options.shuffleStringArray) {
+            this.stringArrayStorage.shuffleStorage();
         }
 
-        const resultNode: ESTree.Node = useStringArray
-            ? this.replaceWithStringArrayCallNode(nodeValue)
-            : this.replaceWithLiteralNode(nodeValue);
-
-        this.nodesCache.set(cacheKey, resultNode);
-
-        return resultNode;
-    }
-
-    /**
-     * @param {string} nodeValue
-     * @returns {boolean}
-     */
-    private canUseStringArray (nodeValue: string): boolean {
-        return (
-            this.options.stringArray &&
-            nodeValue.length >= StringLiteralObfuscatingReplacer.minimumLengthForStringArray &&
-            this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold
-        );
-    }
-
-    /**
-     * @param {string} value
-     * @param {number} stringArrayStorageLength
-     * @returns {IStringArrayIndexData}
-     */
-    private getStringArrayHexadecimalIndex (value: string, stringArrayStorageLength: number): IStringArrayIndexData {
-        if (this.stringLiteralHexadecimalIndexCache.has(value)) {
-            return {
-                fromCache: true,
-                index: <string>this.stringLiteralHexadecimalIndexCache.get(value)
-            };
+        if (this.options.rotateStringArray) {
+            this.stringArrayStorage.rotateStorage();
         }
-
-        const hexadecimalRawIndex: string = NumberUtils.toHex(stringArrayStorageLength);
-        const hexadecimalIndex: string = `${Utils.hexadecimalPrefix}${hexadecimalRawIndex}`;
-
-        this.stringLiteralHexadecimalIndexCache.set(value, hexadecimalIndex);
-
-        return {
-            fromCache: false,
-            index: hexadecimalIndex
-        };
     }
 
     /**
-     * @param {string} value
-     * @returns {IEncodedValue}
+     * @param {SimpleLiteral} literalNode
+     * @returns {Node}
      */
-    private getEncodedValue (value: string): IEncodedValue {
-        let encodedValue: string;
-        let key: string | null = null;
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
 
-        switch (this.options.stringArrayEncoding) {
-            case StringArrayEncoding.Rc4:
-                key = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
-                encodedValue = this.cryptUtils.btoa(this.cryptUtils.rc4(value, key));
+        if (typeof literalValue !== 'string') {
+            throw new Error('`StringLiteralObfuscatingReplacer` should accept only literals with `string` value');
+        }
 
-                break;
+        const stringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.stringArrayStorageAnalyzer
+            .getItemDataForLiteralNode(literalNode);
+        const cacheKey: string = `${literalValue}-${Boolean(stringArrayStorageItemData)}`;
+        const useCachedValue: boolean = this.nodesCache.has(cacheKey) && this.options.stringArrayEncoding !== StringArrayEncoding.Rc4;
 
-            case StringArrayEncoding.Base64:
-                encodedValue = this.cryptUtils.btoa(value);
+        if (useCachedValue) {
+            return <ESTree.Node>this.nodesCache.get(cacheKey);
+        }
 
-                break;
+        const resultNode: ESTree.Node = stringArrayStorageItemData
+            ? this.replaceWithStringArrayCallNode(stringArrayStorageItemData)
+            : this.replaceWithLiteralNode(literalValue);
 
-            default:
-                encodedValue = value;
-        }
+        this.nodesCache.set(cacheKey, resultNode);
 
-        return { encodedValue, key };
+        return resultNode;
     }
 
     /**
@@ -224,36 +144,24 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     }
 
     /**
-     * @param {string} value
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
      * @returns {Node}
      */
-    private replaceWithStringArrayCallNode (value: string): ESTree.Node {
-        const { encodedValue, key }: IEncodedValue = this.getEncodedValue(value);
-        const escapedValue: string = this.escapeSequenceEncoder.encode(encodedValue, this.options.unicodeEscapeSequence);
-
-        const stringArrayStorageLength: number = this.stringArrayStorage.getLength();
-        const stringArrayStorageCallsWrapperName: string = this.stringArrayStorage.getStorageId().split('|')[1];
-
-        const { fromCache, index }: IStringArrayIndexData = this.getStringArrayHexadecimalIndex(
-            escapedValue,
-            stringArrayStorageLength
-        );
-
-        if (!fromCache) {
-            this.stringArrayStorage.set(stringArrayStorageLength, escapedValue);
-        }
+    private replaceWithStringArrayCallNode (stringArrayStorageItemData: IStringArrayStorageItemData): ESTree.Node {
+        const { index, decodeKey } = stringArrayStorageItemData;
 
+        const hexadecimalIndex: string = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(index)}`;
         const callExpressionArgs: (ESTree.Expression | ESTree.SpreadElement)[] = [
-            StringLiteralObfuscatingReplacer.getHexadecimalLiteralNode(index)
+            StringLiteralObfuscatingReplacer.getHexadecimalLiteralNode(hexadecimalIndex)
         ];
 
-        if (key) {
+        if (decodeKey) {
             callExpressionArgs.push(StringLiteralObfuscatingReplacer.getRc4KeyLiteralNode(
-                this.escapeSequenceEncoder.encode(key, this.options.unicodeEscapeSequence)
+                this.escapeSequenceEncoder.encode(decodeKey, this.options.unicodeEscapeSequence)
             ));
         }
 
-        const stringArrayIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(stringArrayStorageCallsWrapperName);
+        const stringArrayIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.stringArrayStorageCallsWrapperName);
 
         // prevent obfuscation of this identifier
         NodeMetadata.set(stringArrayIdentifierNode, { renamedIdentifier: true });

+ 6 - 0
src/options/Options.ts

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

+ 1 - 0
src/options/normalizer-rules/StringArrayRule.ts

@@ -11,6 +11,7 @@ export const StringArrayRule: TOptionsNormalizerRule = (options: IOptions): IOpt
         options = {
             ...options,
             rotateStringArray: false,
+            shuffleStringArray: false,
             stringArray: false,
             stringArrayEncoding: false,
             stringArrayThreshold: 0

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

@@ -27,6 +27,7 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     rotateStringArray: true,
     seed: 0,
     selfDefending: false,
+    shuffleStringArray: true,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapFileName: '',

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

@@ -26,6 +26,7 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     rotateStringArray: false,
     seed: 0,
     selfDefending: false,
+    shuffleStringArray: false,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapFileName: '',

+ 10 - 2
src/storages/ArrayStorage.ts

@@ -54,12 +54,20 @@ export abstract class ArrayStorage <V> implements IArrayStorage <V> {
         this.storageId = this.randomGenerator.getRandomString(6);
     }
 
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    public get (key: number): V | undefined {
+        return this.storage[key];
+    }
+
     /**
      * @param {number} key
      * @returns {V}
      */
-    public get (key: number): V {
-        const value: V | undefined = this.storage[key];
+    public getOrThrow (key: number): V {
+        const value: V | undefined = this.get(key);
 
         if (!value) {
             throw new Error(`No value found in array storage with key \`${key}\``);

+ 10 - 2
src/storages/MapStorage.ts

@@ -49,12 +49,20 @@ export abstract class MapStorage <K, V> implements IMapStorage <K, V> {
         this.storageId = this.randomGenerator.getRandomString(6);
     }
 
+    /**
+     * @param {K} key
+     * @returns {V | undefined}
+     */
+    public get (key: K): V | undefined {
+        return this.storage.get(key);
+    }
+
     /**
      * @param {K} key
      * @returns {V}
      */
-    public get (key: K): V {
-        const value: V | undefined = this.storage.get(key);
+    public getOrThrow (key: K): V {
+        const value: V | undefined = this.get(key);
 
         if (!value) {
             throw new Error(`No value found in map storage with key \`${key}\``);

+ 215 - 15
src/storages/string-array/StringArrayStorage.ts

@@ -1,17 +1,46 @@
 import { inject, injectable, postConstruct } from 'inversify';
 import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
-import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
-import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
+
+import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
+import { ICryptUtils } from '../../interfaces/utils/ICryptUtils';
+import { IEncodedValue } from '../../interfaces/IEncodedValue';
+import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
 import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
 
-import { ArrayStorage } from '../ArrayStorage';
+import { initializable } from '../../decorators/Initializable';
+
+import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+
+import { MapStorage } from '../MapStorage';
 
 @injectable()
-export class StringArrayStorage extends ArrayStorage <string> {
+export class StringArrayStorage extends MapStorage <string, IStringArrayStorageItemData> implements IStringArrayStorage {
+    /**
+     * @type {number}
+     */
+    private static readonly minimumRotationAmount: number = 100;
+
+    /**
+     * @type {number}
+     */
+    private static readonly maximumRotationAmount: number = 500;
+
+    /**
+     * @type {number}
+     */
+    private static readonly rc4KeyLength: number = 4;
+
+    /**
+     * @type {number}
+     */
+    private static readonly rc4KeysCount: number = 50;
+
     /**
      * @type {number}
      */
@@ -22,28 +51,74 @@ export class StringArrayStorage extends ArrayStorage <string> {
      */
     private readonly arrayUtils: IArrayUtils;
 
+    /**
+     * @type {ICryptUtils}
+     */
+    private readonly cryptUtils: ICryptUtils;
+
+    /**
+     * @type {IEscapeSequenceEncoder}
+     */
+    private readonly escapeSequenceEncoder: IEscapeSequenceEncoder;
+
     /**
      * @type {IIdentifierNamesGenerator}
      */
     private readonly identifierNamesGenerator: IIdentifierNamesGenerator;
 
+    /**
+     * @type {string[]}
+     */
+    private readonly rc4Keys: string[];
+
+    /**
+     * @type {number}
+     */
+    private rotationAmount: number = 0;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayStorageName!: string;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayStorageCallsWrapperName!: string;
+
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {IArrayUtils} arrayUtils
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
+     * @param {ICryptUtils} cryptUtils
+     * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
      */
     constructor (
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
         @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.IOptions) options: IOptions
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.ICryptUtils) cryptUtils: ICryptUtils,
+        @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder
     ) {
         super(randomGenerator, options);
 
         this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
         this.arrayUtils = arrayUtils;
+        this.cryptUtils = cryptUtils;
+        this.escapeSequenceEncoder = escapeSequenceEncoder;
+
+        this.rc4Keys = this.randomGenerator.getRandomGenerator()
+            .n(
+                () => this.randomGenerator.getRandomGenerator().string({
+                    length: StringArrayStorage.rc4KeyLength
+                }),
+                StringArrayStorage.rc4KeysCount
+            );
     }
 
     @postConstruct()
@@ -54,25 +129,150 @@ export class StringArrayStorage extends ArrayStorage <string> {
             .generate(StringArrayStorage.stringArrayNameLength);
         const baseStringArrayCallsWrapperName: string = this.identifierNamesGenerator
             .generate(StringArrayStorage.stringArrayNameLength);
-        const stringArrayName: string = `${this.options.identifiersPrefix}${baseStringArrayName}`;
-        const stringArrayCallsWrapperName: string = `${this.options.identifiersPrefix}${baseStringArrayCallsWrapperName}`;
 
-        this.storageId = `${stringArrayName}|${stringArrayCallsWrapperName}`;
+        this.stringArrayStorageName = `${this.options.identifiersPrefix}${baseStringArrayName}`;
+        this.stringArrayStorageCallsWrapperName = `${this.options.identifiersPrefix}${baseStringArrayCallsWrapperName}`;
+
+        this.rotationAmount = this.options.rotateStringArray
+            ? this.randomGenerator.getRandomInteger(
+                StringArrayStorage.minimumRotationAmount,
+                StringArrayStorage.maximumRotationAmount
+            )
+            : 0;
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IStringArrayStorageItemData}
+     */
+    public get (value: string): IStringArrayStorageItemData {
+        return this.getOrSetIfDoesNotExist(value);
+    }
+
+    /**
+     * @returns {number}
+     */
+    public getRotationAmount (): number {
+        return this.rotationAmount;
+    }
+
+    /**
+     * @returns {string}
+     */
+    public getStorageId (): string {
+        return this.stringArrayStorageName;
     }
 
     /**
-     * @param {number} rotationValue
+     * @returns {string}
      */
-    public rotateArray (rotationValue: number): void {
-        this.storage = this.arrayUtils.rotate(this.storage, rotationValue);
+    public getStorageName (): string {
+        return this.getStorageId();
+    }
+
+    /**
+     * @returns {string}
+     */
+    public getStorageCallsWrapperName (): string {
+        return this.stringArrayStorageCallsWrapperName;
+    }
+
+    public rotateStorage (): void {
+        if (!this.getLength()) {
+            return;
+        }
+
+        this.storage = new Map(
+            this.arrayUtils.rotate(
+                Array.from(this.storage.entries()),
+                this.rotationAmount
+            )
+        );
+    }
+
+    public shuffleStorage (): void {
+        this.storage = new Map(
+            this.arrayUtils
+                .shuffle(Array.from(this.storage.entries()))
+                .map<[string, IStringArrayStorageItemData]>(
+                    (
+                        [value, stringArrayStorageItemData]: [string, IStringArrayStorageItemData],
+                        index: number
+                    ) => {
+                        stringArrayStorageItemData.index = index;
+
+                        return [value, stringArrayStorageItemData];
+                    }
+                )
+                .sort((
+                    [, stringArrayStorageItemDataA]: [string, IStringArrayStorageItemData],
+                    [, stringArrayStorageItemDataB]: [string, IStringArrayStorageItemData]
+                ) => stringArrayStorageItemDataA.index - stringArrayStorageItemDataB.index)
+        );
     }
 
     /**
      * @returns {string}
      */
     public toString (): string {
-        return this.storage.map((value: string) => {
-            return `'${value}'`;
-        }).toString();
+        return Array
+            .from(this.storage.values())
+            .map((stringArrayStorageItemData: IStringArrayStorageItemData) => {
+                return `'${this.escapeSequenceEncoder.encode(
+                    stringArrayStorageItemData.encodedValue,
+                    this.options.unicodeEscapeSequence
+                )}'`;
+            }).toString();
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IStringArrayStorageItemData}
+     */
+    private getOrSetIfDoesNotExist (value: string): IStringArrayStorageItemData {
+        const { encodedValue, decodeKey }: IEncodedValue = this.getEncodedValue(value);
+        const storedStringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.storage.get(encodedValue);
+
+        if (storedStringArrayStorageItemData) {
+            return storedStringArrayStorageItemData;
+        }
+
+        const stringArrayStorageItemData: IStringArrayStorageItemData = {
+            encodedValue,
+            decodeKey,
+            value,
+            index: this.getLength()
+        };
+
+        this.storage.set(encodedValue, stringArrayStorageItemData);
+
+        return stringArrayStorageItemData;
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IEncodedValue}
+     */
+    private getEncodedValue (value: string): IEncodedValue {
+        let encodedValue: string;
+        let decodeKey: string | null = null;
+
+        switch (this.options.stringArrayEncoding) {
+            case StringArrayEncoding.Rc4:
+                decodeKey = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
+                encodedValue = this.cryptUtils.btoa(this.cryptUtils.rc4(value, decodeKey));
+
+                break;
+
+            case StringArrayEncoding.Base64:
+                encodedValue = this.cryptUtils.btoa(value);
+
+                break;
+
+            default:
+                encodedValue = value;
+        }
+
+        return { encodedValue, decodeKey };
     }
 }

+ 1 - 1
src/templates/string-array-nodes/string-array-rotate-function-node/StringArrayRotateFunctionTemplate.ts

@@ -11,6 +11,6 @@ export function StringArrayRotateFunctionTemplate (): string {
             };
             
             {code}
-        })({stringArrayName}, 0x{stringArrayRotateValue});
+        })({stringArrayName}, 0x{stringArrayRotationAmount});
     `;
 }

+ 0 - 3
src/types/storages/TStringArrayStorage.d.ts

@@ -1,3 +0,0 @@
-import { IArrayStorage } from '../../interfaces/storages/IArrayStorage';
-
-export type TStringArrayStorage = IArrayStorage <string>;

+ 8 - 4
test/dev/dev.ts

@@ -7,14 +7,18 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-            class Foo {
-                'bar'() {}
-            }
+            var foo = 'foo';
+            var bar = 'bar';
+            var baz = 'baz';
+            var bark = 'bark';
+            
+            console.log(foo, bar, baz, bark);
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
             stringArray: true,
-            stringArrayThreshold: 1
+            stringArrayThreshold: 1,
+            shuffleStringArray: true
         }
     ).getObfuscatedCode();
 

+ 2 - 1
test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts

@@ -1,6 +1,8 @@
 import { assert } from 'chai';
 import { TypeFromEnum } from '@gradecam/tsenum';
 
+import { TInputOptions } from '../../../src/types/options/TInputOptions';
+
 import { IObfuscatedCode } from '../../../src/interfaces/source-code/IObfuscatedCode';
 
 import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode';
@@ -15,7 +17,6 @@ import { IdentifierNamesGenerator } from '../../../src/enums/generators/identifi
 import { buildLargeCode } from '../../helpers/buildLargeCode';
 import { getRegExpMatch } from '../../helpers/getRegExpMatch';
 import { readFileAsString } from '../../helpers/readFileAsString';
-import { TInputOptions } from '../../../src/types/options/TInputOptions';
 
 describe('JavaScriptObfuscator', () => {
     describe('obfuscate', () => {

+ 58 - 15
test/functional-tests/node-transformers/obfuscating-transformers/literal-transformer/LiteralTransformer.spec.ts

@@ -6,6 +6,7 @@ import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncodin
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 
 import { readFileAsString } from '../../../../helpers/readFileAsString';
+import { getRegExpMatch } from '../../../../helpers/getRegExpMatch';
 
 import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
 
@@ -214,26 +215,68 @@ describe('LiteralTransformer', () => {
         });
 
         describe('Variant #9: rc4 encoding', () => {
-            const regExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0', *'.{4}'\);/;
+            describe('Variant #1: single string literal', () => {
+                const regExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0', *'.{4}'\);/;
 
-            let obfuscatedCode: string;
+                let obfuscatedCode: string;
 
-            before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
 
-                obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                    code,
-                    {
-                        ...NO_ADDITIONAL_NODES_PRESET,
-                        stringArray: true,
-                        stringArrayEncoding: StringArrayEncoding.Rc4,
-                        stringArrayThreshold: 1
-                    }
-                ).getObfuscatedCode();
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: StringArrayEncoding.Rc4,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, regExp);
+                });
             });
 
-            it('should replace literal node value with value from string array encoded using rc4', () => {
-                assert.match(obfuscatedCode, regExp);
+            describe('Variant #2: multiple string literals', () => {
+                const variableRegExp1: RegExp = /var *test *= *_0x(?:[a-f0-9]){4}\('0x0', *'(.{4})'\);/;
+                const variableRegExp2: RegExp = /var *test *= *_0x(?:[a-f0-9]){4}\('0x1', *'(.{4})'\);/;
+
+                let encodedLiteralValue1: string;
+                let encodedLiteralValue2: string;
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/same-literal-values.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            seed: 1, // set seed to prevent rare case when all encoded values are the same
+                            stringArray: true,
+                            stringArrayEncoding: StringArrayEncoding.Rc4,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    encodedLiteralValue1 = getRegExpMatch(obfuscatedCode, variableRegExp1);
+                    encodedLiteralValue2 = getRegExpMatch(obfuscatedCode, variableRegExp2);
+                });
+
+                it('Match #1: should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, variableRegExp1);
+                });
+
+                it('Match #2: should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, variableRegExp2);
+                });
+
+                it('Should encode same values as two different encoded string array items', () => {
+                    assert.notEqual(encodedLiteralValue1, encodedLiteralValue2);
+                });
             });
         });
 

+ 27 - 3
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.spec.ts

@@ -7,12 +7,12 @@ import { readFileAsString } from '../../../../../helpers/readFileAsString';
 import { JavaScriptObfuscator } from '../../../../../../src/JavaScriptObfuscatorFacade';
 
 describe('BaseIdentifierObfuscatingReplacer', () => {
-    describe('Base rule', () => {
-        describe('Variant #1: default behaviour', () => {
+    describe('Reserved names', () => {
+        describe('Variant #1: ignore local reserved names', () => {
             let obfuscatedCode: string;
 
             before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+                const code: string = readFileAsString(__dirname + '/fixtures/local-reserved-names.js');
 
                 obfuscatedCode = JavaScriptObfuscator.obfuscate(
                     code,
@@ -30,5 +30,29 @@ describe('BaseIdentifierObfuscatingReplacer', () => {
                 );
             });
         });
+
+        describe('Variant #1: ignore global reserved names', () => {
+            let obfuscatedCode: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/global-reserved-names.js');
+
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        renameGlobals: true,
+                        reservedNames: ['[abc|ghi]']
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('Should keep reserved names without transformations when `reservedNames` option is enabled', () => {
+                assert.match(
+                    obfuscatedCode,
+                    /var *abc *= *0x1; *var *_0x([a-f0-9]){4,6} *= *0x2; *var *ghi *= *0x3;/
+                );
+            });
+        });
     });
 });

+ 3 - 0
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/global-reserved-names.js

@@ -0,0 +1,3 @@
+var abc = 1;
+var def = 2;
+var ghi = 3;

+ 0 - 0
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/simple-input.js → test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/local-reserved-names.js


+ 4 - 0
test/functional-tests/options/OptionsNormalizer.spec.ts

@@ -541,6 +541,7 @@ describe('OptionsNormalizer', () => {
             before(() => {
                 optionsPreset = getNormalizedOptions({
                     ...getDefaultOptions(),
+                    shuffleStringArray: true,
                     stringArray: false,
                     stringArrayEncoding: StringArrayEncoding.Rc4,
                     stringArrayThreshold: 0.5,
@@ -549,6 +550,7 @@ describe('OptionsNormalizer', () => {
 
                 expectedOptionsPreset = {
                     ...getDefaultOptions(),
+                    shuffleStringArray: false,
                     stringArray: false,
                     stringArrayEncoding: false,
                     stringArrayThreshold: 0,
@@ -584,6 +586,7 @@ describe('OptionsNormalizer', () => {
                 optionsPreset = getNormalizedOptions({
                     ...getDefaultOptions(),
                     rotateStringArray: true,
+                    shuffleStringArray: true,
                     stringArray: true,
                     stringArrayThreshold: 0
                 });
@@ -591,6 +594,7 @@ describe('OptionsNormalizer', () => {
                 expectedOptionsPreset = {
                     ...getDefaultOptions(),
                     rotateStringArray: false,
+                    shuffleStringArray: false,
                     stringArray: false,
                     stringArrayThreshold: 0
                 };

+ 308 - 0
test/functional-tests/storages/string-array-storage/StringArrayStorage.spec.ts

@@ -0,0 +1,308 @@
+import { assert } from 'chai';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
+
+import { readFileAsString } from '../../../helpers/readFileAsString';
+
+import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';
+
+describe('StringArrayStorage', () => {
+    describe('Rotate string array', function () {
+        this.timeout(100000);
+
+        describe('Variant #1: single string array value', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 1;
+
+            const stringArrayVariant1RegExp1: RegExp = /var *_0x([a-f0-9]){4} *= *\['test'];/g;
+            const literalNodeVariant1RegExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0'\);/g;
+
+            let stringArrayVariant1Probability: number,
+                literalNodeVariant1Probability: number;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/one-string.js');
+
+                let stringArrayVariant1MatchesLength: number = 0;
+                let literalNodeVariant1MatchesLength: number = 0;
+
+               for (let i = 0; i < samples; i++) {
+                   const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                       code,
+                       {
+                           ...NO_ADDITIONAL_NODES_PRESET,
+                           rotateStringArray: true,
+                           stringArray: true,
+                           stringArrayThreshold: 1
+                       }
+                   ).getObfuscatedCode();
+
+                   if (obfuscatedCode.match(stringArrayVariant1RegExp1)) {
+                       stringArrayVariant1MatchesLength++;
+                   }
+
+                   if (obfuscatedCode.match(literalNodeVariant1RegExp)) {
+                       literalNodeVariant1MatchesLength++;
+                   }
+               }
+
+                stringArrayVariant1Probability = stringArrayVariant1MatchesLength / samples;
+                literalNodeVariant1Probability = literalNodeVariant1MatchesLength / samples;
+            });
+
+            describe('String array probability', () => {
+                it('Variant #1: should create single string array variant', () => {
+                    assert.closeTo(stringArrayVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+
+            describe('Literal node probability', () => {
+                it('Variant #1: should replace literal node with call to string array variant', () => {
+                    assert.closeTo(literalNodeVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+        });
+
+        describe('Variant #2: Three string array values', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedStringArrayVariantProbability: number = 0.33;
+            const expectedLiteralNodeVariantProbability: number = 1;
+
+            const stringArrayVariantsCount: number = 3;
+            const literalNodeVariantsCount: number = 1;
+
+            const stringArrayVariantRegExps: RegExp[] = [
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'bar', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'baz', *'foo'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'foo', *'bar'];/g
+            ];
+            const literalNodeVariantRegExps: RegExp[] = [
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                )
+            ];
+
+            const stringArrayVariantProbabilities: number[] = new Array(stringArrayVariantsCount).fill(0);
+            const literalNodeVariantProbabilities: number[] = new Array(literalNodeVariantsCount).fill(0);
+
+            const stringArrayVariantMatchesLength: number[] = new Array(stringArrayVariantsCount).fill(0);
+            const literalNodeVariantMatchesLength: number[] = new Array(literalNodeVariantsCount).fill(0);
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/three-strings.js');
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            rotateStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                        if (obfuscatedCode.match(stringArrayVariantRegExps[variantIndex])) {
+                            stringArrayVariantMatchesLength[variantIndex]++;
+                        }
+
+                        if (obfuscatedCode.match(literalNodeVariantRegExps[variantIndex])) {
+                            literalNodeVariantMatchesLength[variantIndex]++;
+                        }
+                    }
+                }
+
+                for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                    stringArrayVariantProbabilities[variantIndex] = stringArrayVariantMatchesLength[variantIndex] / samples;
+                }
+
+                for (let variantIndex = 0; variantIndex < literalNodeVariantsCount; variantIndex++) {
+                    literalNodeVariantProbabilities[variantIndex] = literalNodeVariantMatchesLength[variantIndex] / samples;
+                }
+            });
+
+            describe('String array probability', () => {
+                for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                    const variantNumber: number = variantIndex + 1;
+
+                    it(`Variant #${variantNumber}: should create string array variant`, () => {
+                        assert.closeTo(stringArrayVariantProbabilities[variantIndex], expectedStringArrayVariantProbability, delta);
+                    });
+                }
+            });
+
+            describe('Literal node probability', () => {
+                for (let variantIndex = 0; variantIndex < literalNodeVariantsCount; variantIndex++) {
+                    const variantNumber: number = variantIndex + 1;
+
+                    it(`Variant #${variantNumber}: should replace literal node with call to string array variant`, () => {
+                        assert.closeTo(literalNodeVariantProbabilities[variantIndex], expectedLiteralNodeVariantProbability, delta);
+                    });
+                }
+            });
+        });
+    });
+
+    describe('Shuffle string array', function () {
+        this.timeout(100000);
+
+        describe('Variant #1: single string array value', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 1;
+
+            const stringArrayVariantRegExp1: RegExp = /var *_0x([a-f0-9]){4} *= *\['test'];/g;
+            const literalNodeVariant1RegExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0'\);/g;
+
+            let stringArrayVariant1Probability: number,
+                literalNodeVariant1Probability: number;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/one-string.js');
+
+                let stringArrayVariant1MatchesLength: number = 0;
+                let literalNodeVariant1MatchesLength: number = 0;
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            shuffleStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    if (obfuscatedCode.match(stringArrayVariantRegExp1)) {
+                        stringArrayVariant1MatchesLength++;
+                    }
+
+                    if (obfuscatedCode.match(literalNodeVariant1RegExp)) {
+                        literalNodeVariant1MatchesLength++;
+                    }
+                }
+
+                stringArrayVariant1Probability = stringArrayVariant1MatchesLength / samples;
+                literalNodeVariant1Probability = literalNodeVariant1MatchesLength / samples;
+            });
+
+            describe('String array probability', () => {
+                it('Variant #1: should create string array variant', () => {
+                    assert.closeTo(stringArrayVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+
+            describe('Literal node probability', () => {
+                it('Variant #1: should replace literal node with call to string array variant', () => {
+                    assert.closeTo(literalNodeVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+        });
+
+        describe('Variant #2: Three string array values', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 0.166;
+
+            const variantsCount: number = 6;
+
+            const stringArrayVariantRegExps: RegExp[] = [
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'bar', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'baz', *'bar'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'foo', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'baz', *'foo'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'foo', *'bar'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'bar', *'foo'];/g
+            ];
+
+            const literalNodeVariantRegExps: RegExp[] = [
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x1'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x0'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x1'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x0'\\);`
+                )
+            ];
+
+            const stringArrayVariantProbabilities: number[] = new Array(variantsCount).fill(0);
+            const literalNodeVariantProbabilities: number[] = new Array(variantsCount).fill(0);
+
+            const stringArrayVariantMatchesLength: number[] = new Array(variantsCount).fill(0);
+            const literalNodeVariantMatchesLength: number[] = new Array(variantsCount).fill(0);
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/three-strings.js');
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            shuffleStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                        if (obfuscatedCode.match(stringArrayVariantRegExps[variantIndex])) {
+                            stringArrayVariantMatchesLength[variantIndex]++;
+                        }
+
+                        if (obfuscatedCode.match(literalNodeVariantRegExps[variantIndex])) {
+                            literalNodeVariantMatchesLength[variantIndex]++;
+                        }
+                    }
+                }
+
+                for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                    stringArrayVariantProbabilities[variantIndex] = stringArrayVariantMatchesLength[variantIndex] / samples;
+                    literalNodeVariantProbabilities[variantIndex] = literalNodeVariantMatchesLength[variantIndex] / samples;
+                }
+
+            });
+
+            for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                const variantNumber: number = variantIndex + 1;
+
+                it(`Variant #${variantNumber}: should create string array variant`, () => {
+                    assert.closeTo(stringArrayVariantProbabilities[variantIndex], expectedVariantProbability, delta);
+                });
+
+                it(`Variant #${variantNumber}: should replace literal node with call to string array variant`, () => {
+                    assert.closeTo(literalNodeVariantProbabilities[variantIndex], expectedVariantProbability, delta);
+                });
+            }
+        });
+    });
+});

+ 1 - 0
test/functional-tests/storages/string-array-storage/fixtures/one-string.js

@@ -0,0 +1 @@
+var test = 'test';

+ 3 - 0
test/functional-tests/storages/string-array-storage/fixtures/three-strings.js

@@ -0,0 +1,3 @@
+var foo = 'foo';
+var bar = 'bar';
+var baz = 'baz';

+ 1 - 1
test/helpers/getRegExpMatch.ts

@@ -8,7 +8,7 @@ export function getRegExpMatch (str: string, regExp: RegExp, matchIndex: number
     const match: RegExpMatchArray | null = str.match(regExp);
 
     if (!match) {
-        throw new Error(`No matches were found for regular expression \`${regExp.toString()}\``);
+        throw new Error(`No matches were found. String: \`${str}\`, regular expression: \`${regExp.toString()}\``);
     }
 
     return (<RegExpMatchArray>match)[matchIndex + 1];

+ 5 - 0
test/index.spec.ts

@@ -7,6 +7,7 @@ require('source-map-support').install();
  */
 import './unit-tests/analyzers/calls-graph-analyzer/CallsGraphAnalyzer.spec';
 import './unit-tests/analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer.spec';
+import './unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec';
 import './unit-tests/cli/sanitizers/ArraySanitizer.spec';
 import './unit-tests/cli/sanitizers/BooleanSanitizer.spec';
 import './unit-tests/cli/sanitizers/IdentifierNamesGeneratorSanitizer.spec';
@@ -29,6 +30,9 @@ import './unit-tests/node/node-lexical-scope-utils/NodeLexicalScopeUtils.spec';
 import './unit-tests/node/node-statement-utils/NodeStatementUtils.spec';
 import './unit-tests/node/node-utils/NodeUtils.spec';
 import './unit-tests/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec';
 import './unit-tests/options/ValidationErrorsFormatter.spec';
 import './unit-tests/source-code/ObfuscatedCode.spec';
 import './unit-tests/storages/ArrayStorage.spec';
@@ -84,6 +88,7 @@ import './functional-tests/node-transformers/preparing-transformers/obfuscating-
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec';
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/reserved-string-obfuscating-guard/ReservedStringObfuscatingGuard.spec';
 import './functional-tests/options/OptionsNormalizer.spec';
+import './functional-tests/storages/string-array-storage/StringArrayStorage.spec';
 import './functional-tests/templates/debug-protection-nodes/DebugProtectionFunctionCallTemplate.spec';
 import './functional-tests/templates/domain-lock-nodes/DomainLockNodeTemplate.spec';
 import './functional-tests/templates/GlobalVariableNoEvalTemplate.spec';

+ 318 - 0
test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts

@@ -0,0 +1,318 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { ServiceIdentifiers } from '../../../../src/container/ServiceIdentifiers';
+
+import { TInputOptions } from '../../../../src/types/options/TInputOptions';
+
+import { IInversifyContainerFacade } from '../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IStringArrayStorageAnalyzer } from '../../../../src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../../../src/interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { InversifyContainerFacade } from '../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../src/node/NodeFactory';
+import { NodeMetadata } from '../../../../src/node/NodeMetadata';
+
+const getStringArrayStorageAnalyzer = (options: TInputOptions): IStringArrayStorageAnalyzer => {
+    const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
+
+    inversifyContainerFacade.load('', '', options);
+
+    return inversifyContainerFacade.get<IStringArrayStorageAnalyzer>(ServiceIdentifiers.IStringArrayStorageAnalyzer);
+};
+
+describe('StringArrayStorageAnalyzer', () => {
+    let stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
+
+    describe('analyze', () => {
+        describe('Base analyze of the AST tree', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+            const literalNode3: ESTree.Literal = NodeFactory.literalNode('baz');
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: IStringArrayStorageItemData = {
+                encodedValue: 'bar',
+                decodeKey: null,
+                index: 1,
+                value: 'bar'
+            };
+            const expectedStringArrayStorageItemData3: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData3: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                stringArrayStorageItemData3 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode3);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+
+            it('Variant #3: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData3, expectedStringArrayStorageItemData3);
+            });
+        });
+
+        describe('Base analyze of the AST tree with string literal nodes with values shorter than allowed length', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('ba');
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Base analyze of the AST tree with number literal nodes', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode(1);
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Analyzes of the AST tree with ignored nodes', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+            NodeMetadata.set(literalNode2, {ignoredNode: true});
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        /**
+         * This test covers rare case when with random value inside `shouldAddValueToStringArray` was `0`
+         * that trigger positive check for method.
+         *
+         * As fix i added check of `this.options.stringArray` option value
+         */
+        describe('Analyzes of the AST tree with disabled string array', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+            const expectedStringArrayStorageItemData1: undefined = undefined;
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArray: false,
+                    stringArrayThreshold: 1
+                });
+                (<any>stringArrayStorageAnalyzer).options.stringArrayThreshold = 1;
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Analyzes of the AST tree with string array threshold', () => {
+            describe('Threshold value: 0', () => {
+                const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+                const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+                const expectedStringArrayStorageItemData1: undefined = undefined;
+                const expectedStringArrayStorageItemData2: undefined = undefined;
+
+                let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+                let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+                before(() => {
+                    stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                        stringArrayThreshold: 0
+                    });
+
+                    const astTree: ESTree.Program = NodeFactory.programNode([
+                        NodeFactory.expressionStatementNode(literalNode1),
+                        NodeFactory.expressionStatementNode(literalNode2)
+                    ]);
+
+                    stringArrayStorageAnalyzer.analyze(astTree);
+                    stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                    stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                });
+
+                it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+                });
+
+                it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+                });
+            });
+
+            describe('Threshold value: 0.5', () => {
+                const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+                const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+                const expectedStringArrayStorageItemData1: undefined = undefined;
+                const expectedStringArrayStorageItemData2: IStringArrayStorageItemData = {
+                    encodedValue: 'bar',
+                    decodeKey: null,
+                    index: 0,
+                    value: 'bar'
+                };
+
+                let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+                let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+                before(() => {
+                    stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                        stringArrayThreshold: 0.5,
+                        seed: 1
+                    });
+
+                    const astTree: ESTree.Program = NodeFactory.programNode([
+                        NodeFactory.expressionStatementNode(literalNode1),
+                        NodeFactory.expressionStatementNode(literalNode2)
+                    ]);
+
+                    stringArrayStorageAnalyzer.analyze(astTree);
+                    stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                    stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                });
+
+                it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+                });
+
+                it('Variant #2: should return correct string array storage item data for literal node #2', () => {
+                    assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('BooleanLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.BooleanLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `boolean` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(true);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `boolean` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `boolean` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `boolean` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('NumberLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.NumberLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `number` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(1);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `number` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `number` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `number` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('StringLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.StringLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `string` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `string` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `string` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(1);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `string` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 101 - 14
test/unit-tests/storages/ArrayStorage.spec.ts

@@ -74,6 +74,23 @@ describe('ArrayStorage', () => {
         });
     });
 
+    describe('getStorageId', () => {
+        const storageIdRegExp: RegExp = /^[a-zA-Z0-9]{6}$/;
+
+        let storageId: string;
+
+        before(() => {
+            storage = getStorageInstance<string>();
+            storage.set(storageKey, storageValue);
+
+            storageId = storage.getStorageId();
+        });
+
+        it('should return storage id', () => {
+            assert.match(storageId, storageIdRegExp);
+        });
+    });
+
     describe('get', () => {
         describe('Variant #1: value exist', () => {
             const expectedValue: string = storageValue;
@@ -92,6 +109,41 @@ describe('ArrayStorage', () => {
             });
         });
 
+        describe('Variant #2: value isn\'t exist', () => {
+            const expectedValue: undefined = undefined;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+
+                value = storage.get(storageKey);
+            });
+
+            it('should return undefined if value does not exist in the storage', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+    });
+
+    describe('getOrThrow', () => {
+        describe('Variant #1: value exist', () => {
+            const expectedValue: string = storageValue;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                value = storage.getOrThrow(storageKey);
+            });
+
+            it('should return value from storage by key', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+
         describe('Variant #2: value isn\'t exist', () => {
             const expectedError: ErrorConstructor = Error;
 
@@ -100,7 +152,7 @@ describe('ArrayStorage', () => {
             before(() => {
                 storage = getStorageInstance<string>();
 
-                testFunc = () => storage.get(storageKey);
+                testFunc = () => storage.getOrThrow(storageKey);
             });
 
             it('should throw an error', () => {
@@ -194,27 +246,62 @@ describe('ArrayStorage', () => {
     });
 
     describe('mergeWith', () => {
-        const secondStorageKey: number = 1;
-        const secondStorageValue: string = 'bar';
+        describe('Base merge', () => {
+            const secondStorageKey: number = 1;
+            const secondStorageValue: string = 'bar';
 
-        const expectedArray: string[] = [storageValue, secondStorageValue];
+            const expectedArray: string[] = [storageValue, secondStorageValue];
 
-        let array: string[];
+            let array: string[];
 
-        before(() => {
-            storage = getStorageInstance<string>();
-            storage.set(storageKey, storageValue);
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
+                secondStorage.set(secondStorageKey, secondStorageValue);
 
-            const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
-            secondStorage.set(secondStorageKey, secondStorageValue);
+                storage.mergeWith(secondStorage, false);
 
-            storage.mergeWith(secondStorage, false);
+                array = storage.getStorage();
+            });
 
-            array = storage.getStorage();
+            it('should merge two storages', () => {
+                assert.deepEqual(array, expectedArray);
+            });
         });
 
-        it('should merge two storages', () => {
-            assert.deepEqual(array, expectedArray);
+        describe('Merge with storage id', () => {
+            const secondStorageKey: number = 1;
+            const secondStorageValue: string = 'bar';
+
+            const expectedArray: string[] = [storageValue, secondStorageValue];
+
+            let array: string[];
+            let storageId: string;
+            let expectedStorageId: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
+                expectedStorageId = secondStorage.getStorageId();
+                secondStorage.set(secondStorageKey, secondStorageValue);
+
+                storage.mergeWith(secondStorage, true);
+
+                storageId = storage.getStorageId();
+                array = storage.getStorage();
+            });
+
+            it('should update storage id', () => {
+                assert.deepEqual(storageId, expectedStorageId);
+            });
+
+            it('should merge two storages', () => {
+                assert.deepEqual(array, expectedArray);
+            });
         });
     });
 });

+ 37 - 2
test/unit-tests/storages/MapStorage.spec.ts

@@ -91,6 +91,41 @@ describe('MapStorage', () => {
             });
         });
 
+        describe('Variant #2: value isn\'t exist', () => {
+            const expectedValue: undefined = undefined;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+
+                value = storage.get(storageKey);
+            });
+
+            it('should return undefined', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+    });
+
+    describe('getOrThrow', () => {
+        describe('Variant #1: value exist', () => {
+            const expectedValue: string = storageValue;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                value = storage.getOrThrow(storageKey);
+            });
+
+            it('should return value from storage by key', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+
         describe('Variant #2: value isn\'t exist', () => {
             const expectedError: ErrorConstructor = Error;
 
@@ -99,7 +134,7 @@ describe('MapStorage', () => {
             before(() => {
                 storage = getStorageInstance<string>();
 
-                testFunc = () => storage.get(storageKey);
+                testFunc = () => storage.getOrThrow(storageKey);
             });
 
             it('should throw an error', () => {
@@ -219,7 +254,7 @@ describe('MapStorage', () => {
             storage = getStorageInstance<string>();
             storage.set(storageKey, storageValue);
 
-            value = storage.get(storageKey);
+            value = storage.getOrThrow(storageKey);
         });
 
         it('should set value to the storage', () => {

部分文件因为文件数量过多而无法显示