1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import {
17  forEachChild,
18  getLeadingCommentRangesOfNode,
19  isCallExpression,
20  isExpressionStatement,
21  isIdentifier,
22  isStructDeclaration,
23  SyntaxKind,
24  visitEachChild
25} from 'typescript';
26
27import type {
28  CommentRange,
29  Identifier,
30  Node,
31  SourceFile,
32  StructDeclaration,
33  TransformationContext 
34} from 'typescript';
35import type { IOptions } from '../configs/IOptions';
36import { LocalVariableCollections, PropCollections, UnobfuscationCollections } from './CommonCollections';
37import { historyUnobfuscatedNamesMap } from '../transformers/rename/RenameIdentifierTransformer';
38
39export interface ReservedNameInfo {
40  universalReservedArray: RegExp[]; // items contain wildcards
41  specificReservedArray: string[]; // items do not contain wildcards
42}
43
44/**
45 * collect exist identifier names in current source file
46 * @param sourceFile
47 */
48export function collectExistNames(sourceFile: SourceFile): Set<string> {
49  const identifiers: Set<string> = new Set<string>();
50
51  let visit = (node: Node): void => {
52    if (isIdentifier(node)) {
53      identifiers.add(node.text);
54    }
55
56    forEachChild(node, visit);
57  };
58
59  forEachChild(sourceFile, visit);
60  return identifiers;
61}
62
63type IdentifiersAndStructs = {shadowIdentifiers: Identifier[], shadowStructs: StructDeclaration[]};
64
65/**
66 * collect exist identifiers in current source file
67 * @param sourceFile
68 * @param context
69 */
70export function collectIdentifiersAndStructs(sourceFile: SourceFile, context: TransformationContext): IdentifiersAndStructs {
71  const identifiers: Identifier[] = [];
72  const structs: StructDeclaration[] = [];
73
74  let visit = (node: Node): Node => {
75    if (isStructDeclaration(node)) {
76      structs.push(node);
77    }
78    // @ts-ignore
79    if (getOriginalNode(node).virtual) {
80      return node;
81    }
82    if (!isIdentifier(node) || !node.parent) {
83      return visitEachChild(node, visit, context);
84    }
85
86    identifiers.push(node);
87    return node;
88  };
89
90  visit(sourceFile);
91  return {shadowIdentifiers: identifiers, shadowStructs: structs};
92}
93
94export function isCommentedNode(node: Node, sourceFile: SourceFile): boolean {
95  const ranges: CommentRange[] = getLeadingCommentRangesOfNode(node, sourceFile);
96  return ranges !== undefined;
97}
98
99export function isSuperCallStatement(node: Node): boolean {
100  return isExpressionStatement(node) &&
101    isCallExpression(node.expression) &&
102    node.expression.expression.kind === SyntaxKind.SuperKeyword;
103}
104
105/**
106 * separate wildcards from specific items.
107 */
108export function separateUniversalReservedItem(originalArray: string[]): ReservedNameInfo {
109  if (!originalArray) {
110    throw new Error('Unable to handle the empty array.');
111  }
112  const reservedInfo: ReservedNameInfo = {
113    universalReservedArray: [],
114    specificReservedArray: []
115  };
116
117  originalArray.forEach(reservedItem => {
118    if (containWildcards(reservedItem)) {
119      const regexPattern = wildcardTransformer(reservedItem);
120      const regexOperator = new RegExp(`^${regexPattern}$`);
121      reservedInfo.universalReservedArray.push(regexOperator);
122      recordWildcardMapping(reservedItem, regexOperator);
123    } else {
124      reservedInfo.specificReservedArray.push(reservedItem);
125    }
126  });
127  return reservedInfo;
128}
129
130function recordWildcardMapping(originString: string, regExpression: RegExp): void {
131  if (UnobfuscationCollections.printKeptName) {
132    UnobfuscationCollections.reservedWildcardMap.set(regExpression, originString);
133  }
134}
135
136/**
137 * check if the item contains '*', '?'.
138 */
139export function containWildcards(item: string): boolean {
140  return /[\*\?]/.test(item);
141}
142
143/**
144 * Convert specific characters into regular expressions.
145 */
146export function wildcardTransformer(wildcard: string, isPath?: boolean): string {
147  // Add an escape character in front of special characters
148  // special characters: '\', '^', '$', '.', '+', '|', '[', ']', '{', '}', '(', ')'
149  let escapedItem = wildcard.replace(/[\\+^${}()|\[\]\.]/g, '\\$&');
150
151  // isPath: containing '**', and '*', '?' can not be matched with '/'. 
152  if (isPath) {
153    // before: ../**/a/b/c*/?.ets
154    // after: ../.*/a/b/c[^/]*/[^/].ets
155    return escapedItem.replace(/\*\*/g, '.*').replace(/(?<!\.)\*/g, '[^/]*').replace(/\?/g, '[^/]');
156  }
157  // before: *a?
158  // after: .*a.
159  return escapedItem.replace(/\*/g, '.*').replace(/\?/g, '.');
160}
161
162/**
163 * Determine whether the original name needs to be preserved.
164 */
165export function needToBeReserved(reservedSet: Set<string>, universalArray: RegExp[], originalName: string): boolean {
166  return reservedSet.has(originalName) || isMatchWildcard(universalArray, originalName);
167}
168
169/**
170 * Determine whether it can match the wildcard character in the array.
171 */
172export function isMatchWildcard(wildcardArray: RegExp[], item: string): boolean {
173  for (const wildcard of wildcardArray) {
174    if (wildcard.test(item)) {
175      return true;
176    }
177  }
178  return false;
179}
180
181/**
182 * Separate parts of an array that contain wildcard characters.
183 */
184export function handleReservedConfig(config: IOptions, optionName: string, reservedListName: string,
185  universalLisName: string, enableRemove?: string): void {
186  const reservedConfig = config?.[optionName];
187  let needSeparate: boolean = !!(reservedConfig?.[reservedListName]);
188  if (enableRemove) {
189    needSeparate &&= reservedConfig[enableRemove];
190  }
191  if (needSeparate) {
192    // separate items which contain wildcards from others
193    const reservedInfo: ReservedNameInfo = separateUniversalReservedItem(reservedConfig[reservedListName]);
194    reservedConfig[reservedListName] = reservedInfo.specificReservedArray;
195    reservedConfig[universalLisName] = reservedInfo.universalReservedArray;
196  }
197}
198
199export function isReservedLocalVariable(mangledName: string): boolean {
200  return LocalVariableCollections.reservedLangForLocal.has(mangledName) || 
201    LocalVariableCollections.reservedConfig?.has(mangledName) ||
202    LocalVariableCollections.reservedStruct?.has(mangledName) ||
203    UnobfuscationCollections.reservedSdkApiForProp?.has(mangledName) ||
204    UnobfuscationCollections.reservedExportName?.has(mangledName);
205}
206
207export function isReservedTopLevel(originalName: string): boolean {
208  if (PropCollections.enablePropertyObfuscation) {
209    return isReservedProperty(originalName);
210  }
211
212  // The 'mReservedToplevelNames' has already been added to 'PropCollections.reservedProperties'.
213  return UnobfuscationCollections.reservedLangForTopLevel.has(originalName) ||
214    UnobfuscationCollections.reservedSdkApiForGlobal?.has(originalName) ||
215    UnobfuscationCollections.reservedExportName?.has(originalName) ||
216    PropCollections.reservedProperties?.has(originalName) ||
217    isMatchWildcard(PropCollections.universalReservedProperties, originalName);
218}
219
220export function isReservedProperty(originalName: string): boolean {
221  return UnobfuscationCollections.reservedSdkApiForProp?.has(originalName) ||
222    UnobfuscationCollections.reservedLangForProperty?.has(originalName) ||
223    UnobfuscationCollections.reservedStruct?.has(originalName) ||
224    UnobfuscationCollections.reservedExportNameAndProp?.has(originalName) ||
225    UnobfuscationCollections.reservedStrProp?.has(originalName) ||
226    UnobfuscationCollections.reservedEnum?.has(originalName) ||
227    PropCollections.reservedProperties?.has(originalName) ||
228    isMatchWildcard(PropCollections.universalReservedProperties, originalName);
229}
230
231  /**
232   * Reasons for not being obfuscated.
233   */
234export enum WhitelistType {
235  SDK = 'sdk',
236  LANG = 'lang',
237  CONF = 'conf',
238  STRUCT = 'struct',
239  EXPORT = 'exported',
240  STRPROP = 'strProp',
241  ENUM = 'enum'
242}
243
244function needToRecordTopLevel(originalName: string, recordMap: Map<string, Set<string>>, nameWithScope: string): boolean {
245  if (PropCollections.enablePropertyObfuscation) {
246    return needToRecordProperty(originalName, recordMap, nameWithScope);
247  }
248
249  let reservedFlag = false;
250  if (UnobfuscationCollections.reservedLangForTopLevel.has(originalName)) {
251    recordReservedName(nameWithScope, WhitelistType.LANG, recordMap);
252    reservedFlag = true;
253  }
254
255  if (UnobfuscationCollections.reservedSdkApiForGlobal?.has(originalName)) {
256    recordReservedName(nameWithScope, WhitelistType.SDK, recordMap);
257    reservedFlag = true;
258  }
259
260  if (UnobfuscationCollections.reservedExportName?.has(originalName)) {
261    recordReservedName(nameWithScope, WhitelistType.EXPORT, recordMap);
262    reservedFlag = true;
263  }
264
265  // The 'mReservedToplevelNames' has already been added to 'PropCollections.reservedProperties'.
266  if (PropCollections.reservedProperties?.has(originalName) ||
267    isMatchWildcard(PropCollections.universalReservedProperties, originalName)) {
268    recordReservedName(nameWithScope, WhitelistType.CONF, recordMap);
269    reservedFlag = true;
270  }
271
272  return reservedFlag;
273}
274
275function needToReservedLocal(originalName: string, recordMap: Map<string, Set<string>>, nameWithScope: string): boolean {
276  let reservedFlag = false;
277
278  if (LocalVariableCollections.reservedLangForLocal.has(originalName)) {
279    recordReservedName(nameWithScope, WhitelistType.LANG, recordMap);
280    reservedFlag = true;
281  }
282
283  if (UnobfuscationCollections.reservedSdkApiForLocal?.has(originalName)) {
284    recordReservedName(nameWithScope, WhitelistType.SDK, recordMap);
285    reservedFlag = true;
286  }
287
288  if (UnobfuscationCollections.reservedExportName?.has(originalName)) {
289    recordReservedName(nameWithScope, WhitelistType.EXPORT, recordMap);
290    reservedFlag = true;
291  }
292
293  if (LocalVariableCollections.reservedConfig?.has(originalName)) {
294    recordReservedName(nameWithScope, WhitelistType.CONF, recordMap);
295    reservedFlag = true;
296  }
297
298  if (LocalVariableCollections.reservedStruct?.has(originalName)) {
299    recordReservedName(nameWithScope, WhitelistType.STRUCT, recordMap);
300    reservedFlag = true;
301  }
302
303  return reservedFlag;
304}
305
306/**
307 * If the property name is in the whitelist, record the reason for not being obfuscated.
308 * @param nameWithScope: If both property obfuscation and top-level obfuscation or export obfuscation are enabled,
309 * this interface is also used to record the reasons why the top-level names or export names were not obfuscated,
310 * and the top-level names or export names include the scope.
311 */
312export function needToRecordProperty(originalName: string, recordMap?: Map<string, Set<string>>, nameWithScope?: string): boolean {
313  let reservedFlag = false;
314  let recordName = nameWithScope ? nameWithScope : originalName;
315  if (UnobfuscationCollections.reservedSdkApiForProp?.has(originalName)) {
316    recordReservedName(recordName, WhitelistType.SDK, recordMap);
317    reservedFlag = true;
318  }
319
320  if (UnobfuscationCollections.reservedLangForProperty?.has(originalName)) {
321    recordReservedName(recordName, WhitelistType.LANG, recordMap);
322    reservedFlag = true;
323  }
324
325  if (UnobfuscationCollections.reservedStruct?.has(originalName)) {
326    recordReservedName(recordName, WhitelistType.STRUCT, recordMap);
327    reservedFlag = true;
328  }
329
330  if (UnobfuscationCollections.reservedExportNameAndProp?.has(originalName)) {
331    recordReservedName(recordName, WhitelistType.EXPORT, recordMap);
332    reservedFlag = true;
333  }
334
335  if (UnobfuscationCollections.reservedStrProp?.has(originalName)) {
336    recordReservedName(recordName, WhitelistType.STRPROP, recordMap);
337    reservedFlag = true;
338  }
339
340  if (UnobfuscationCollections.reservedEnum?.has(originalName)) {
341    recordReservedName(recordName, WhitelistType.ENUM, recordMap);
342    reservedFlag = true;
343  }
344
345  if (PropCollections.reservedProperties?.has(originalName) ||
346    isMatchWildcard(PropCollections.universalReservedProperties, originalName)) {
347    recordReservedName(recordName, WhitelistType.CONF, recordMap);
348    reservedFlag = true;
349  }
350
351  return reservedFlag;
352}
353
354export function isInTopLevelWhitelist(originalName: string, recordMap: Map<string, Set<string>>, nameWithScope: string): boolean {
355  if (UnobfuscationCollections.printKeptName) {
356    return needToRecordTopLevel(originalName, recordMap, nameWithScope);
357  }
358
359  return isReservedTopLevel(originalName);
360}
361
362export function isInPropertyWhitelist(originalName: string, recordMap: Map<string, Set<string>>): boolean {
363  if (UnobfuscationCollections.printKeptName) {
364    return needToRecordProperty(originalName, recordMap);
365  }
366
367  return isReservedProperty(originalName);
368}
369
370export function isInLocalWhitelist(originalName: string, recordMap: Map<string, Set<string>>, nameWithScope: string): boolean {
371  if (UnobfuscationCollections.printKeptName) {
372    return needToReservedLocal(originalName, recordMap, nameWithScope);
373  }
374
375  return isReservedLocalVariable(originalName);
376}
377
378export function recordReservedName(originalName: string, type: string, recordObj?: Map<string, Set<string>>): void {
379  if (!UnobfuscationCollections.printKeptName || !recordObj) {
380    return;
381  }
382  if (!recordObj.has(originalName)) {
383    recordObj.set(originalName, new Set());
384  }
385  recordObj.get(originalName).add(type);
386}
387
388export function recordHistoryUnobfuscatedNames(nameWithScope: string): void {
389  if (historyUnobfuscatedNamesMap?.has(nameWithScope)) {
390    UnobfuscationCollections.unobfuscatedNamesMap.set(nameWithScope,
391      new Set(historyUnobfuscatedNamesMap.get(nameWithScope)));
392  }
393}