• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 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 { ApiExtractor } from '../common/ApiExtractor';
17import { FileUtils } from './FileUtils';
18import { AtKeepCollections, UnobfuscationCollections } from './CommonCollections';
19import * as crypto from 'crypto';
20import * as ts from 'typescript';
21import fs from 'fs';
22import path from 'path';
23
24export function addToSet<T>(targetSet: Set<T>, sourceSet: Set<T>): void {
25  sourceSet.forEach((element) => targetSet.add(element));
26}
27
28export function arrayToSet<T>(array: T[]): Set<T> {
29  return new Set(array);
30}
31
32export function setToArray<T>(set: Set<T>): T[] {
33  return Array.from(set);
34}
35
36export function areSetsEqual<T>(setA: Set<T>, setB: Set<T>): boolean {
37  if (setA.size !== setB.size) {
38    return false;
39  }
40  for (const setItem of setA) {
41    if (!setB.has(setItem)) {
42      return false;
43    }
44  }
45  return true;
46}
47
48export const SOURCE_FILE_PATHS: string = 'sourceFilePaths.cache';
49export const TRANSFORMED_PATH: string = 'transformed';
50export const FILE_NAMES_MAP: string = 'transformedFileNamesMap.json';
51export const FILE_WHITE_LISTS: string = 'fileWhiteLists.json';
52export const PROJECT_WHITE_LIST: string = 'projectWhiteList.json';
53
54export interface KeepInfo {
55  propertyNames: Set<string>;
56  globalNames: Set<string>;
57}
58
59/**
60 * Informantion of build files
61 */
62export interface ModuleInfo {
63  content: string;
64  /**
65   * The path in build cache dir
66   */
67  buildFilePath: string;
68  /**
69   * The `originSourceFilePath` relative to project root dir.
70   */
71  relativeSourceFilePath: string;
72  /**
73   * The origin source file path will be set with rollup moduleId when obfuscate intermediate js source code,
74   * whereas be set with tsc node.fileName when obfuscate intermediate ts source code.
75   */
76  originSourceFilePath?: string;
77  rollupModuleId?: string;
78}
79
80export interface FileContent {
81  moduleInfo: ModuleInfo;
82  previousStageSourceMap?: ts.RawSourceMap;
83}
84
85/**
86 * We have these structure to store project white list and file white lists,
87 * project white list is merged by each file white list.
88 *
89 * We have keepinfo and reservedinfo in white lists:
90 *  KeepInfo: names that we cannot obfuscate or obfuscate as.
91 *  ReservedInfo: names that we cannot obfuscate as.
92 *
93 * ProjectWhiteList
94 * ├── projectKeepInfo: ProjectKeepInfo
95 * │   ├── propertyNames: Set<string>
96 * │   └── globalNames: Set<string>
97 * └── projectReservedInfo: ProjectReservedInfo
98 *     ├── enumProperties: Set<string>
99 *     └── propertyParams: Set<string>
100 * FileWhiteList
101 * ├── fileKeepInfo: FileKeepInfo
102 * │   ├── keepSymbol?: KeepInfo (optional)
103 * │   │   ├── propertyNames: Set<string>
104 * │   │   └── globalNames: Set<string>
105 * │   ├── keepAsConsumer?: KeepInfo (optional)
106 * │   │   ├── propertyNames: Set<string>
107 * │   │   └── globalNames: Set<string>
108 * │   ├── structProperties: Set<string>
109 * │   ├── exported: KeepInfo
110 * │   │   ├── propertyNames: Set<string>
111 * │   │   └── globalNames: Set<string>
112 * │   ├── enumProperties: Set<string>
113 * │   └── stringProperties: Set<string>
114 * └── fileReservedInfo: FileReservedInfo
115 *     ├── enumProperties: Set<string>
116 *     └── propertyParams: Set<string>
117 */
118export interface FileKeepInfo {
119  keepSymbol?: KeepInfo; // Names marked with "@KeepSymbol".
120  keepAsConsumer?: KeepInfo; // Names marked with "@KeepAsConsumer".
121  structProperties: Set<string>; // Struct properties collected from struct.
122  exported: KeepInfo; // Exported names and properties.
123  enumProperties: Set<string>; // Enum properties.
124  stringProperties: Set<string>; // String properties.
125}
126
127export interface FileReservedInfo {
128  enumProperties: Set<string>; // Enum members, collected when has initializer.
129  propertyParams: Set<string>; // Properties parameters in constructor.
130}
131
132export interface FileWhiteList {
133  fileKeepInfo: FileKeepInfo;
134  fileReservedInfo: FileReservedInfo;
135}
136
137export interface ProjectKeepInfo {
138  propertyNames: Set<string>;
139  globalNames: Set<string>;
140}
141
142export interface ProjectReservedInfo {
143  enumProperties: Set<string>;
144  propertyParams: Set<string>;
145}
146
147export interface ProjectWhiteList {
148  projectKeepInfo: ProjectKeepInfo;
149  projectReservedInfo: ProjectReservedInfo;
150}
151
152export interface ProjectWhiteListJsonData {
153  projectKeepInfo: {
154    propertyNames: Array<string>;
155    globalNames: Array<string>;
156  };
157  projectReservedInfo: {
158    enumProperties: Array<string>;
159    propertyParams: Array<string>;
160  };
161}
162
163
164// The object to manage project white lists, should be initialized in arkObfuscator
165export let projectWhiteListManager: ProjectWhiteListManager | undefined;
166
167// Initialize projectWhiteListManager
168export function initProjectWhiteListManager(cachePath: string, isIncremental: boolean, enableAtKeep: boolean): void {
169  projectWhiteListManager = new ProjectWhiteListManager(cachePath, isIncremental, enableAtKeep);
170}
171
172// Clear projectWhiteListManager
173export function clearProjectWhiteListManager(): void {
174  projectWhiteListManager = undefined;
175}
176
177/**
178 * This class is used to manage project white lists.
179 * Used to collect white list of each file and merge them into project white lists.
180 */
181export class ProjectWhiteListManager {
182  // If atKeep is enabled
183  private enableAtKeep: boolean;
184
185  // Cache path for file white lists
186  private fileWhiteListsCachePath: string;
187
188  // Cache path for project white lists
189  private projectWhiteListCachePath: string;
190
191  // If it is incremental compliation
192  private isIncremental: boolean = false;
193
194  // If we should reObfuscate all files
195  private shouldReObfuscate: boolean = false;
196
197  // White lists of each file
198  private fileWhiteListMap: Map<string, FileWhiteList>;
199
200  // White lists for one file
201  public fileWhiteListInfo: FileWhiteList | undefined;
202
203  public getEnableAtKeep(): boolean {
204    return this.enableAtKeep;
205  }
206
207  public getFileWhiteListsCachePath(): string {
208    return this.fileWhiteListsCachePath;
209  }
210
211  public getProjectWhiteListCachePath(): string {
212    return this.projectWhiteListCachePath;
213  }
214
215  public getIsIncremental(): boolean {
216    return this.isIncremental;
217  }
218
219  public getShouldReObfuscate(): boolean {
220    return this.shouldReObfuscate;
221  }
222
223  public getFileWhiteListMap(): Map<string, FileWhiteList> {
224    return this.fileWhiteListMap;
225  }
226
227  constructor(cachePath: string, isIncremental: boolean, enableAtKeep: boolean) {
228    this.fileWhiteListsCachePath = path.join(cachePath, FILE_WHITE_LISTS);
229    this.projectWhiteListCachePath = path.join(cachePath, PROJECT_WHITE_LIST);
230    this.isIncremental = isIncremental;
231    this.enableAtKeep = enableAtKeep;
232    this.fileWhiteListMap = new Map();
233  }
234
235  private createDefaultFileKeepInfo(): FileKeepInfo {
236    return {
237      structProperties: new Set<string>(),
238      exported: {
239        propertyNames: new Set<string>(),
240        globalNames: new Set<string>(),
241      },
242      enumProperties: new Set<string>(),
243      stringProperties: new Set<string>(),
244    };
245  }
246
247  private createDefaultFileReservedInfo(): FileReservedInfo {
248    return {
249      enumProperties: new Set<string>(),
250      propertyParams: new Set<string>(),
251    };
252  }
253
254  // Create one fileWhilteList object, with keepSymbol & keepAsConsumer if atKeep is enabled
255  public createFileWhiteList(): FileWhiteList {
256    const fileKeepInfo = this.enableAtKeep
257      ? {
258          keepSymbol: {
259            propertyNames: new Set<string>(),
260            globalNames: new Set<string>(),
261          },
262          keepAsConsumer: {
263            propertyNames: new Set<string>(),
264            globalNames: new Set<string>(),
265          },
266          ...this.createDefaultFileKeepInfo(),
267        }
268      : this.createDefaultFileKeepInfo();
269    return {
270      fileKeepInfo,
271      fileReservedInfo: this.createDefaultFileReservedInfo(),
272    };
273  }
274
275  // Initialize current collector,
276  // should be called before we collect fileWhiteLists.
277  public setCurrentCollector(path: string): void {
278    const unixPath = FileUtils.toUnixPath(path);
279    let fileWhiteListInfo: FileWhiteList | undefined = this.fileWhiteListMap.get(unixPath);
280    if (!fileWhiteListInfo) {
281      fileWhiteListInfo = this.createFileWhiteList();
282      this.fileWhiteListMap.set(unixPath, fileWhiteListInfo);
283    }
284    this.fileWhiteListInfo = fileWhiteListInfo;
285  }
286
287  private readFileWhiteLists(filePath: string): Map<string, FileWhiteList> {
288    const fileContent = fs.readFileSync(filePath, 'utf8');
289    const parsed: Object = JSON.parse(fileContent);
290
291    const map = new Map<string, FileWhiteList>();
292    for (const key in parsed) {
293      if (Object.prototype.hasOwnProperty.call(parsed, key)) {
294        const fileKeepInfo: FileKeepInfo = {
295          keepSymbol: parsed[key].fileKeepInfo.keepSymbol
296            ? {
297                propertyNames: arrayToSet(parsed[key].fileKeepInfo.keepSymbol.propertyNames),
298                globalNames: arrayToSet(parsed[key].fileKeepInfo.keepSymbol.globalNames),
299              }
300            : undefined,
301          keepAsConsumer: parsed[key].fileKeepInfo.keepAsConsumer
302            ? {
303                propertyNames: arrayToSet(parsed[key].fileKeepInfo.keepAsConsumer.propertyNames),
304                globalNames: arrayToSet(parsed[key].fileKeepInfo.keepAsConsumer.globalNames),
305              }
306            : undefined,
307          structProperties: arrayToSet(parsed[key].fileKeepInfo.structProperties),
308          exported: {
309            propertyNames: arrayToSet(parsed[key].fileKeepInfo.exported.propertyNames),
310            globalNames: arrayToSet(parsed[key].fileKeepInfo.exported.globalNames),
311          },
312          enumProperties: arrayToSet(parsed[key].fileKeepInfo.enumProperties),
313          stringProperties: arrayToSet(parsed[key].fileKeepInfo.stringProperties),
314        };
315
316        const fileReservedInfo: FileReservedInfo = {
317          enumProperties: arrayToSet(parsed[key].fileReservedInfo.enumProperties),
318          propertyParams: arrayToSet(parsed[key].fileReservedInfo.propertyParams),
319        };
320
321        map.set(key, { fileKeepInfo, fileReservedInfo });
322      }
323    }
324
325    return map;
326  }
327
328  private writeFileWhiteLists(filePath: string, fileWhiteLists: Map<string, FileWhiteList>): void {
329    const jsonData: Object = {};
330    for (const [key, value] of fileWhiteLists) {
331      jsonData[key] = {
332        fileKeepInfo: {
333          keepSymbol: value.fileKeepInfo.keepSymbol
334            ? {
335                propertyNames: setToArray(value.fileKeepInfo.keepSymbol.propertyNames),
336                globalNames: setToArray(value.fileKeepInfo.keepSymbol.globalNames),
337              }
338            : undefined,
339          keepAsConsumer: value.fileKeepInfo.keepAsConsumer
340            ? {
341                propertyNames: setToArray(value.fileKeepInfo.keepAsConsumer.propertyNames),
342                globalNames: setToArray(value.fileKeepInfo.keepAsConsumer.globalNames),
343              }
344            : undefined,
345          structProperties: setToArray(value.fileKeepInfo.structProperties),
346          exported: {
347            propertyNames: setToArray(value.fileKeepInfo.exported.propertyNames),
348            globalNames: setToArray(value.fileKeepInfo.exported.globalNames),
349          },
350          enumProperties: setToArray(value.fileKeepInfo.enumProperties),
351          stringProperties: setToArray(value.fileKeepInfo.stringProperties),
352        },
353        fileReservedInfo: {
354          enumProperties: setToArray(value.fileReservedInfo.enumProperties),
355          propertyParams: setToArray(value.fileReservedInfo.propertyParams),
356        },
357      };
358    }
359
360    const jsonString = JSON.stringify(jsonData, null, 2);
361    fs.writeFileSync(filePath, jsonString, 'utf8');
362  }
363
364  private readProjectWhiteList(filePath: string): ProjectWhiteList {
365    const fileContent = fs.readFileSync(filePath, 'utf8');
366    const parsed: ProjectWhiteListJsonData = JSON.parse(fileContent);
367
368    const projectKeepInfo: ProjectKeepInfo = {
369      propertyNames: arrayToSet(parsed.projectKeepInfo.propertyNames),
370      globalNames: arrayToSet(parsed.projectKeepInfo.globalNames),
371    };
372
373    const projectReservedInfo: ProjectReservedInfo = {
374      enumProperties: arrayToSet(parsed.projectReservedInfo.enumProperties),
375      propertyParams: arrayToSet(parsed.projectReservedInfo.propertyParams),
376    };
377
378    return {
379      projectKeepInfo,
380      projectReservedInfo,
381    };
382  }
383
384  private writeProjectWhiteList(filePath: string, projectWhiteList: ProjectWhiteList): void {
385    const jsonData: ProjectWhiteListJsonData = {
386      projectKeepInfo: {
387        propertyNames: setToArray(projectWhiteList.projectKeepInfo.propertyNames),
388        globalNames: setToArray(projectWhiteList.projectKeepInfo.globalNames),
389      },
390      projectReservedInfo: {
391        enumProperties: setToArray(projectWhiteList.projectReservedInfo.enumProperties),
392        propertyParams: setToArray(projectWhiteList.projectReservedInfo.propertyParams),
393      },
394    };
395
396    const jsonString = JSON.stringify(jsonData, null, 2);
397    fs.writeFileSync(filePath, jsonString, 'utf8');
398  }
399
400  public updateFileWhiteListMap(deletedFilePath?: Set<string>): void {
401    const lastFileWhiteLists: Map<string, FileWhiteList> = this.readFileWhiteLists(this.fileWhiteListsCachePath);
402
403    deletedFilePath?.forEach((path) => {
404      lastFileWhiteLists.delete(path);
405    });
406    this.fileWhiteListMap.forEach((value, key) => {
407      lastFileWhiteLists.set(key, value);
408    });
409    this.writeFileWhiteLists(this.fileWhiteListsCachePath, lastFileWhiteLists);
410    this.fileWhiteListMap = lastFileWhiteLists;
411  }
412
413  public createProjectWhiteList(fileWhiteLists: Map<string, FileWhiteList>): ProjectWhiteList {
414    const projectKeepInfo: ProjectKeepInfo = {
415      propertyNames: new Set(),
416      globalNames: new Set(),
417    };
418
419    const projectReservedInfo: ProjectReservedInfo = {
420      enumProperties: new Set(),
421      propertyParams: new Set(),
422    };
423
424    fileWhiteLists.forEach((fileWhiteList) => {
425      // 1. Collect fileKeepInfo
426      // Collect keepSymbol
427      fileWhiteList.fileKeepInfo.keepSymbol?.globalNames.forEach((globalName) => {
428        projectKeepInfo.globalNames.add(globalName);
429      });
430      fileWhiteList.fileKeepInfo.keepSymbol?.propertyNames.forEach((propertyName) => {
431        projectKeepInfo.propertyNames.add(propertyName);
432      });
433
434      // Collect keepAsConsumer
435      fileWhiteList.fileKeepInfo.keepAsConsumer?.globalNames.forEach((globalName) => {
436        projectKeepInfo.globalNames.add(globalName);
437      });
438      fileWhiteList.fileKeepInfo.keepAsConsumer?.propertyNames.forEach((propertyName) => {
439        projectKeepInfo.propertyNames.add(propertyName);
440      });
441
442      // Collect structProperties
443      fileWhiteList.fileKeepInfo.structProperties.forEach((propertyName) => {
444        projectKeepInfo.propertyNames.add(propertyName);
445      });
446
447      // Collect exportedNames
448      fileWhiteList.fileKeepInfo.exported.globalNames.forEach((globalName) => {
449        projectKeepInfo.globalNames.add(globalName);
450      });
451      fileWhiteList.fileKeepInfo.exported.propertyNames.forEach((propertyName) => {
452        projectKeepInfo.propertyNames.add(propertyName);
453      });
454
455      // Collect enumProperties
456      fileWhiteList.fileKeepInfo.enumProperties.forEach((propertyName) => {
457        projectKeepInfo.propertyNames.add(propertyName);
458      });
459
460      // Collect stringProperties
461      fileWhiteList.fileKeepInfo.stringProperties.forEach((propertyName) => {
462        projectKeepInfo.propertyNames.add(propertyName);
463      });
464
465      // 2. Collect fileReservedInfo
466      // Collect enumProperties
467      fileWhiteList.fileReservedInfo.enumProperties.forEach((enumPropertyName) => {
468        projectReservedInfo.enumProperties.add(enumPropertyName);
469      });
470
471      // Collect propertyParams
472      fileWhiteList.fileReservedInfo.propertyParams.forEach((propertyParam) => {
473        projectReservedInfo.propertyParams.add(propertyParam);
474      });
475    });
476
477    const projectWhiteList = {
478      projectKeepInfo: projectKeepInfo,
479      projectReservedInfo: projectReservedInfo,
480    };
481
482    return projectWhiteList;
483  }
484
485  // Determine if the project's white list has been updated.
486  private areProjectWhiteListsEqual(whiteList1: ProjectWhiteList, whiteList2: ProjectWhiteList): boolean {
487    const projectKeepInfoEqual =
488      areSetsEqual(whiteList1.projectKeepInfo.propertyNames, whiteList2.projectKeepInfo.propertyNames) &&
489      areSetsEqual(whiteList1.projectKeepInfo.globalNames, whiteList2.projectKeepInfo.globalNames);
490
491    if (!projectKeepInfoEqual) {
492      return false;
493    }
494
495    const projectReservedInfoEqual =
496      areSetsEqual(whiteList1.projectReservedInfo.enumProperties, whiteList2.projectReservedInfo.enumProperties) &&
497      areSetsEqual(whiteList1.projectReservedInfo.propertyParams, whiteList2.projectReservedInfo.propertyParams);
498
499    return projectReservedInfoEqual;
500  }
501
502  // We only scan updated files or newly created files in incremental compliation,
503  // so we need to update the UnobfuscationCollections and reserved names use all file white lists.
504  // This should be called in incremental compliation after we updated file white list.
505  private updateUnobfuscationCollections(): void {
506    this.fileWhiteListMap.forEach((fileWhiteList) => {
507      if (this.enableAtKeep) {
508        addToSet(AtKeepCollections.keepSymbol.propertyNames, fileWhiteList.fileKeepInfo.keepSymbol.propertyNames);
509        addToSet(AtKeepCollections.keepSymbol.globalNames, fileWhiteList.fileKeepInfo.keepSymbol.globalNames);
510        addToSet(AtKeepCollections.keepAsConsumer.propertyNames, fileWhiteList.fileKeepInfo.keepAsConsumer.propertyNames);
511        addToSet(AtKeepCollections.keepAsConsumer.globalNames, fileWhiteList.fileKeepInfo.keepAsConsumer.globalNames);
512      }
513      addToSet(UnobfuscationCollections.reservedStruct, fileWhiteList.fileKeepInfo.structProperties);
514      addToSet(UnobfuscationCollections.reservedEnum, fileWhiteList.fileKeepInfo.enumProperties);
515      addToSet(UnobfuscationCollections.reservedExportName, fileWhiteList.fileKeepInfo.exported.globalNames);
516      addToSet(UnobfuscationCollections.reservedExportNameAndProp, fileWhiteList.fileKeepInfo.exported.propertyNames);
517      addToSet(UnobfuscationCollections.reservedStrProp, fileWhiteList.fileKeepInfo.stringProperties);
518      addToSet(ApiExtractor.mConstructorPropertySet, fileWhiteList.fileReservedInfo.propertyParams);
519      addToSet(ApiExtractor.mEnumMemberSet, fileWhiteList.fileReservedInfo.enumProperties);
520    });
521  }
522
523  private updateProjectWhiteList(): ProjectWhiteList {
524    const lastProjectWhiteList: ProjectWhiteList = this.readProjectWhiteList(this.projectWhiteListCachePath);
525    const newestProjectWhiteList: ProjectWhiteList = this.createProjectWhiteList(this.fileWhiteListMap);
526    if (!this.areProjectWhiteListsEqual(lastProjectWhiteList, newestProjectWhiteList)) {
527      this.writeProjectWhiteList(this.projectWhiteListCachePath, newestProjectWhiteList);
528      this.shouldReObfuscate = true;
529    }
530    return newestProjectWhiteList;
531  }
532
533  public createOrUpdateWhiteListCaches(deletedFilePath?: Set<string>): void {
534    let projectWhiteList: ProjectWhiteList;
535    if (!this.isIncremental) {
536      this.writeFileWhiteLists(this.fileWhiteListsCachePath, this.fileWhiteListMap);
537      projectWhiteList = this.createProjectWhiteList(this.fileWhiteListMap);
538      this.writeProjectWhiteList(this.projectWhiteListCachePath, projectWhiteList);
539    } else {
540      this.updateFileWhiteListMap(deletedFilePath);
541      projectWhiteList = this.updateProjectWhiteList();
542      this.updateUnobfuscationCollections();
543    }
544    this.fileWhiteListMap.clear();
545  }
546}
547
548/**
549 * This class is used to manage sourceFilePath.json file.
550 * Will be created when initialize arkObfuscator.
551 */
552export class FilePathManager {
553  // Stores all source files paths
554  private sourceFilePaths: Set<string>;
555
556  // Files deleted in incremental build
557  private deletedSourceFilePaths: Set<string>;
558
559  // Files added in incremental build
560  private addedSourceFilePaths: Set<string>;
561
562  // Cache path of sourceFilePaths.cache file
563  private filePathsCache: string;
564
565  public getSourceFilePaths(): Set<string> {
566    return this.sourceFilePaths;
567  }
568
569  public getDeletedSourceFilePaths(): Set<string> {
570    return this.deletedSourceFilePaths;
571  }
572
573  public getAddedSourceFilePaths(): Set<string> {
574    return this.addedSourceFilePaths;
575  }
576
577  public getFilePathsCache(): string {
578    return this.filePathsCache;
579  }
580
581  constructor(cachePath: string) {
582    this.filePathsCache = path.join(cachePath, SOURCE_FILE_PATHS);
583    this.sourceFilePaths = new Set();
584    this.deletedSourceFilePaths = new Set();
585    this.addedSourceFilePaths = new Set();
586  }
587
588  private setSourceFilePaths(sourceFilePaths: Set<string>): void {
589    sourceFilePaths.forEach((filePath) => {
590      if (!FileUtils.isReadableFile(filePath) || !ApiExtractor.isParsableFile(filePath)) {
591        return;
592      }
593      this.sourceFilePaths.add(filePath);
594    });
595  }
596
597  private writeSourceFilePaths(): void {
598    const content = Array.from(this.sourceFilePaths).join('\n');
599    FileUtils.writeFile(this.filePathsCache, content);
600  }
601
602  // Update sourceFilePaths.cache and get deleted file list and added file list
603  private updateSourceFilePaths(): void {
604    const cacheContent = FileUtils.readFile(this.filePathsCache).split('\n');
605    const cacheSet = new Set(cacheContent);
606
607    for (const path of cacheSet) {
608      if (!this.sourceFilePaths.has(path)) {
609        this.deletedSourceFilePaths.add(path);
610      }
611    }
612
613    for (const path of this.sourceFilePaths) {
614      if (!cacheSet.has(path)) {
615        this.addedSourceFilePaths.add(path);
616      }
617    }
618
619    this.writeSourceFilePaths();
620  }
621
622  // Create or update sourceFilePaths.cache
623  public createOrUpdateSourceFilePaths(sourceFilePaths: Set<string>): void {
624    this.setSourceFilePaths(sourceFilePaths);
625    if (this.isIncremental()) {
626      this.updateSourceFilePaths();
627    } else {
628      this.writeSourceFilePaths();
629    }
630  }
631
632  public isIncremental(): boolean {
633    return fs.existsSync(this.filePathsCache);
634  }
635}
636
637/**
638 * This class is used to manage transfromed file content.
639 * Will be created when initialize arkObfuscator.
640 */
641export class FileContentManager {
642  // Path that stores all transfromed file content
643  private transformedFilesDir: string;
644
645  // Path of fileNamesMap
646  private fileNamesMapPath: string;
647
648  // Stores [originpath -> transformed cache path]
649  public fileNamesMap: Map<string, string>;
650
651  // If it is incremental compilation
652  private isIncremental: boolean;
653
654  public getTransformedFilesDir(): string {
655    return this.transformedFilesDir;
656  }
657
658  public getFileNamesMapPath(): string {
659    return this.fileNamesMapPath;
660  }
661
662  public getIsIncremental(): boolean {
663    return this.isIncremental;
664  }
665
666  constructor(cachePath: string, isIncremental: boolean) {
667    this.transformedFilesDir = path.join(cachePath, TRANSFORMED_PATH);
668    this.fileNamesMapPath = path.join(this.transformedFilesDir, FILE_NAMES_MAP);
669    this.fileNamesMap = new Map();
670    this.isIncremental = isIncremental;
671    FileUtils.createDirectory(this.transformedFilesDir);
672  }
673
674  // Generate hash from filePah
675  private generatePathHash(filePath: string): string {
676    const hash = crypto.createHash('md5');
677    hash.update(filePath);
678    return hash.digest('hex').slice(0, 16);
679  }
680
681  // Generate new file name for transfromed sourcefiles.
682  // Generated from origin filePath's hash & time stamp.
683  private generateFileName(filePath: string): string {
684    const hash = this.generatePathHash(filePath);
685    const timestamp = Date.now().toString();
686    return `${hash}_${timestamp}`;
687  }
688
689  // Delete a set of files, used in incremental compliation if we have deleted some files
690  public deleteFileContent(deletedSourceFilePaths: Set<string>): void {
691    deletedSourceFilePaths.forEach((filePath) => {
692      this.deleteTransformedFile(filePath);
693    });
694  }
695
696  // Delete transfromed file
697  private deleteTransformedFile(deletedSourceFilePath: string): void {
698    if (this.fileNamesMap.has(deletedSourceFilePath)) {
699      const transformedFilePath: string = this.fileNamesMap.get(deletedSourceFilePath);
700      this.fileNamesMap.delete(deletedSourceFilePath);
701      FileUtils.deleteFile(transformedFilePath);
702    }
703  }
704
705  public readFileNamesMap(): void {
706    const jsonObject = FileUtils.readFileAsJson(this.fileNamesMapPath);
707    this.fileNamesMap = new Map<string, string>(Object.entries(jsonObject));
708  }
709
710  public writeFileNamesMap(): void {
711    const jsonObject = Object.fromEntries(this.fileNamesMap.entries());
712    const jsonString = JSON.stringify(jsonObject, null, 2);
713    FileUtils.writeFile(this.fileNamesMapPath, jsonString);
714    this.fileNamesMap.clear();
715  }
716
717  public readFileContent(transformedFilePath: string): FileContent {
718    const fileContentJson = FileUtils.readFile(transformedFilePath);
719    const fileConstent: FileContent = JSON.parse(fileContentJson);
720    return fileConstent;
721  }
722
723  private writeFileContent(filePath: string, fileContent: FileContent): void {
724    const jsonString = JSON.stringify(fileContent, null, 2);
725    FileUtils.writeFile(filePath, jsonString);
726  }
727
728  // Update file content for newly created files or updated files
729  public updateFileContent(fileContent: FileContent): void {
730    const originPath = FileUtils.toUnixPath(fileContent.moduleInfo.originSourceFilePath);
731    if (this.isIncremental) {
732      this.deleteTransformedFile(originPath);
733    }
734    const fileName = this.generateFileName(originPath);
735    const transformedFilePath = path.join(this.transformedFilesDir, fileName);
736    this.fileNamesMap.set(originPath, transformedFilePath);
737    this.writeFileContent(transformedFilePath, fileContent);
738  }
739
740  public getSortedFiles(): string[] {
741    return (this.fileNamesMap && Array.from(this.fileNamesMap.keys())).sort((a, b) => a.localeCompare(b)) || [];
742  }
743}
744