1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5Copyright (c) 2024 Huawei Device Co., Ltd.
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10    http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17"""
18
19from grammar_test import Runner, parse_args, Task
20import os
21import json
22import copy
23import shutil
24import stat
25
26combination_config_path = os.path.join(os.path.dirname(__file__), 'combination_config.json')
27config_temp_dir = os.path.join(os.path.dirname(__file__), '../test/local/temp_configs')
28INDENTATION = 2
29TEST_TYPE = 'combinations'
30
31DEFAULT_CONFIG = {
32    'mCompact': False,
33    'mRemoveComments': False,
34    'mOutputDir': '',
35    'mDisableConsole': False,
36    'mSimplify': False,
37    'mNameObfuscation': {
38        'mEnable': True,
39        'mNameGeneratorType': 1,
40        'mDictionaryList': [],
41        'mRenameProperties': False,
42        'mKeepStringProperty': True,
43        'mTopLevel': False
44    },
45    'mExportObfuscation': False,
46    'mEnableSourceMap': False,
47    'mEnableNameCache': False,
48    'mKeepFileSourceCode': {
49        'mKeepSourceOfPaths': [],
50        'mkeepFilesAndDependencies': []
51    },
52    'mRenameFileName': {
53        'mEnable': False,
54        'mNameGeneratorType': 1,
55        'mReservedFileNames': [],
56        'mOhmUrlUseNormalized': False
57    }
58}
59
60# This alias is used as the output directory for the current obfuscation options.
61CONFIG_ALIAS = {
62    'mCompact': 'comp',
63    'mRemoveComments': 'rmComments',
64    'mDisableConsole': 'con',
65    'mNameObfuscation': {
66        'mEnable': 'localVar',
67        'mRenameProperties': 'prop',
68        'mKeepStringProperty': 'strProp',
69        'mTopLevel': 'top'
70    },
71    'mExportObfuscation': 'export',
72    'mEnableSourceMap': 'sourcemap',
73    'mEnableNameCache': 'namecache'
74}
75
76
77class CombinationRunner(Runner):
78    def __init__(self, test_filter, root_dir, test_type):
79        super().__init__(test_filter, root_dir, test_type)
80        self.configs = []
81        self.combinations = []
82        self.temp_files = []
83        self.obfscated_cache_root_dir = os.path.normpath(root_dir)
84        if not os.path.exists(config_temp_dir):
85            os.makedirs(config_temp_dir)
86
87    def prepare_task(self, input_dirs, config_path):
88        for input_dir in input_dirs:
89            input_abs_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), input_dir))
90            for one_case_path in os.listdir(input_abs_dir):
91                input_path = os.path.join(input_abs_dir, one_case_path)
92                task = Task(input_path, config_path, TEST_TYPE)
93                self.obfuscation_tasks.append(task)
94
95    def recursive_search(self, config_part, input_part, result):
96        for key in input_part:
97            if key not in config_part:
98                continue
99            input_val = input_part[key]
100            config_val = config_part[key]
101
102            if isinstance(input_val, dict):
103                self.recursive_search(config_val, input_val, result)
104            else:
105                result.append(config_val)
106
107    def get_alias_from_config(self, config, input_data):
108        result = []
109        self.recursive_search(config, input_data, result)
110        return '+'.join(result) if result else 'default'
111
112    def merge_deep(self, target, source):
113        for key, value in source.items():
114            if isinstance(value, dict) and key in target:
115                target[key] = self.merge_deep(target.get(key, {}), value)
116            else:
117                target[key] = value
118        return target
119
120    def merge_config(self, options, options_alias, whitelist, output_dir):
121        whitelist_config = whitelist.get(options_alias, {})
122        output_config = {'mOutputDir': output_dir}
123        option_config = self.merge_deep(copy.deepcopy(whitelist_config), copy.deepcopy(options))
124        option_config = self.merge_deep(option_config, output_config)
125        merged_config = self.merge_deep(copy.deepcopy(DEFAULT_CONFIG), option_config)
126        return merged_config
127
128    def generate_config(self, combinations, input_dirs, output_dir, whitelist):
129        output_abs_dir = os.path.join(os.path.dirname(combination_config_path), output_dir)
130        for combination in combinations:
131            alias_str = self.get_alias_from_config(CONFIG_ALIAS, combination)
132            output_dir_for_current_option = os.path.normpath(os.path.join(output_abs_dir, alias_str))
133            temp_config_path = os.path.normpath(os.path.join(config_temp_dir, alias_str + '_config.json'))
134            merged_config = self.merge_config(combination, alias_str, whitelist, str(output_dir_for_current_option))
135            flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
136            modes = stat.S_IRUSR | stat.S_IWUSR
137            # write temp config file
138            with os.fdopen(os.open(temp_config_path, flags, modes), 'w') as config_file:
139                json.dump(merged_config, config_file, indent=INDENTATION)
140            self.temp_files.append(temp_config_path)
141            self.prepare_task(input_dirs, temp_config_path)
142
143
144    def delete_temp_config_files(self):
145        for path in self.temp_files:
146            os.remove(path)
147        self.temp_files.clear()
148
149
150    def combine(self, current, remaining_keys, configs, result):
151        if not remaining_keys:
152            return
153
154        key = remaining_keys[0]
155        value = configs[key]
156
157        if isinstance(value, dict):
158            sub_keys = value.keys()
159            sub_combinations = []
160
161            for sub_key in sub_keys:
162                sub_value = value[sub_key]
163                current_sub_combinations = sub_combinations[0:]
164                for comb in current_sub_combinations:
165                    new_comb = {**comb, sub_key: sub_value}
166                    sub_combinations.append(new_comb)
167                sub_combinations.append({sub_key: sub_value})
168
169            for sub_comb in sub_combinations:
170                new_comb = {**current, key: sub_comb}
171                result.append(new_comb)
172                self.combine(new_comb, remaining_keys[1:], configs, result)
173        else:
174            new_comb = {**current, key: value}
175            result.append(new_comb)
176            self.combine(new_comb, remaining_keys[1:], configs, result)
177
178        self.combine(current, remaining_keys[1:], configs, result)
179
180    def generate_combinations(self, configs):
181        result = [{}]  # Initialize with the empty object
182        keys = list(configs.keys())
183        self.combine({}, keys, configs, result)
184        return result
185
186    def parse_configs_and_execute(self):
187        with open(combination_config_path, 'r', encoding='utf-8') as file:
188            configs = json.load(file)
189        # Parse each configuration in combination_config.json and then execute the test.
190        for key, config in configs.items():
191            enable_options = config.get('enableOptions', {})
192            input_dirs = config.get('inputDirs', [])
193            output_dir = config.get('outputDir', '')
194            whitelist = config.get('whitelist', {})
195            combinations = self.generate_combinations(enable_options)
196            print('Obfuscation option combinations count:', len(combinations))
197            self.generate_config(combinations, input_dirs, output_dir, whitelist)
198            self.obfuscate()
199            self.delete_temp_config_files()
200
201    def try_remove_cache(self):
202        if not self.has_failed_cases():
203            shutil.rmtree(self.obfscated_cache_root_dir)
204
205
206def main():
207    args = parse_args()
208    local_root_dir = os.path.join(os.path.dirname(__file__), "../test/local/combinations")
209    runner = CombinationRunner(args.test_filter, local_root_dir, TEST_TYPE)
210    runner.parse_configs_and_execute()
211    runner.run_with_node()
212    runner.content_compare()
213    runner.print_summary()
214
215    runner.try_remove_cache()
216
217
218if __name__ == '__main__':
219    main()
220