• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 type {
17  ModifiersArray,
18  Node,
19  ParameterDeclaration,
20  SourceFile
21} from 'typescript';
22
23import {
24  createSourceFile,
25  forEachChild,
26  isBinaryExpression,
27  isClassDeclaration,
28  isClassExpression,
29  isStructDeclaration,
30  isExpressionStatement,
31  isEnumDeclaration,
32  isExportAssignment,
33  isExportDeclaration,
34  isExportSpecifier,
35  isIdentifier,
36  isInterfaceDeclaration,
37  isObjectLiteralExpression,
38  isTypeAliasDeclaration,
39  isVariableDeclaration,
40  isVariableStatement,
41  isElementAccessExpression,
42  isPropertyAccessExpression,
43  isStringLiteral,
44  ScriptTarget,
45  SyntaxKind,
46  sys,
47  isConstructorDeclaration,
48  getModifiers
49} from 'typescript';
50
51import fs from 'fs';
52import path from 'path';
53import json5 from 'json5';
54
55import {
56  getClassProperties,
57  getElementAccessExpressionProperties,
58  getEnumProperties, getInterfaceProperties,
59  getObjectProperties,
60  getTypeAliasProperties,
61  isParameterPropertyModifier,
62} from '../utils/OhsUtil';
63import { scanProjectConfig } from './ApiReader';
64import { stringPropsSet } from '../utils/OhsUtil';
65import type { IOptions } from '../configs/IOptions';
66import { FileUtils } from '../utils/FileUtils';
67import { supportedParsingExtension } from './type';
68
69export namespace ApiExtractor {
70  interface KeywordInfo {
71    hasExport: boolean,
72    hasDeclare: boolean
73  }
74
75  export enum ApiType {
76    API = 1,
77    COMPONENT = 2,
78    PROJECT_DEPENDS = 3,
79    PROJECT = 4,
80    CONSTRUCTOR_PROPERTY = 5
81  }
82
83  let mCurrentExportedPropertySet: Set<string> = new Set<string>();
84  let mCurrentExportNameSet: Set<string> = new Set<string>();
85  export let mPropertySet: Set<string> = new Set<string>();
86  export let mLibExportNameSet: Set<string> = new Set<string>();
87  export let mConstructorPropertySet: Set<string> = undefined;
88  /**
89   * filter classes or interfaces with export, default, etc
90   */
91  const getKeyword = function (modifiers: ModifiersArray): KeywordInfo {
92    if (modifiers === undefined) {
93      return {hasExport: false, hasDeclare: false};
94    }
95
96    let hasExport: boolean = false;
97    let hasDeclare: boolean = false;
98
99    for (const modifier of modifiers) {
100      if (modifier.kind === SyntaxKind.ExportKeyword) {
101        hasExport = true;
102      }
103
104      if (modifier.kind === SyntaxKind.DeclareKeyword) {
105        hasDeclare = true;
106      }
107    }
108
109    return {hasExport: hasExport, hasDeclare: hasDeclare};
110  };
111
112  /**
113   * get export name list
114   * @param astNode
115   */
116  const visitExport = function (astNode): void {
117    if (isExportAssignment(astNode)) {
118      if (!mCurrentExportNameSet.has(astNode.expression.getText())) {
119        mCurrentExportNameSet.add(astNode.expression.getText());
120        mCurrentExportedPropertySet.add(astNode.expression.getText());
121      }
122
123      return;
124    }
125
126    let {hasExport, hasDeclare} = getKeyword(astNode.modifiers);
127    if (!hasExport) {
128      addCommonJsExports(astNode);
129      return;
130    }
131
132    if (astNode.name) {
133      if (!mCurrentExportNameSet.has(astNode.name.getText())) {
134        mCurrentExportNameSet.add(astNode.name.getText());
135        mCurrentExportedPropertySet.add(astNode.name.getText());
136      }
137
138      return;
139    }
140
141    if (hasDeclare && astNode.declarationList) {
142      astNode.declarationList.declarations.forEach((declaration) => {
143        const declarationName = declaration.name.getText();
144        if (!mCurrentExportNameSet.has(declarationName)) {
145          mCurrentExportNameSet.add(declarationName);
146          mCurrentExportedPropertySet.add(declarationName);
147        }
148      });
149    }
150  };
151
152  const checkPropertyNeedVisit = function (astNode): boolean {
153    if (astNode.name && !mCurrentExportNameSet.has(astNode.name.getText())) {
154      return false;
155    }
156
157    if (astNode.name === undefined) {
158      let {hasDeclare} = getKeyword(astNode.modifiers);
159      if (hasDeclare && astNode.declarationList &&
160        !mCurrentExportNameSet.has(astNode.declarationList.declarations[0].name.getText())) {
161        return false;
162      }
163    }
164
165    return true;
166  };
167
168  /**
169   * used only in oh sdk api extract or api of xxx.d.ts declaration file
170   * @param astNode
171   */
172  const visitChildNode = function (astNode): void {
173    if (!astNode) {
174      return;
175    }
176
177    if (astNode.name !== undefined && !mCurrentExportedPropertySet.has(astNode.name.getText())) {
178      if (isStringLiteral(astNode.name)) {
179        mCurrentExportedPropertySet.add(astNode.name.text);
180      } else {
181        mCurrentExportedPropertySet.add(astNode.name.getText());
182      }
183    }
184
185    astNode.forEachChild((childNode) => {
186      visitChildNode(childNode);
187    });
188  };
189
190  // Collect constructor properties from all files.
191  const visitNodeForConstructorProperty = function (astNode): void {
192    if (!astNode) {
193      return;
194    }
195
196    if (isConstructorDeclaration) {
197      const visitParam = (param: ParameterDeclaration): void => {
198        const modifiers = getModifiers(param);
199        if (!modifiers || modifiers.length <= 0) {
200          return;
201        }
202
203        const findRet = modifiers.find(modifier => isParameterPropertyModifier(modifier));
204        if (!isIdentifier(param.name) || findRet === undefined) {
205          return;
206        }
207        mConstructorPropertySet?.add(param.name.getText());
208      };
209
210      astNode?.parameters?.forEach((param) => {
211        visitParam(param);
212      });
213    }
214
215    astNode.forEachChild((childNode) => {
216      visitNodeForConstructorProperty(childNode);
217    });
218  };
219  /**
220   * visit ast of a file and collect api list
221   * used only in oh sdk api extract
222   * @param astNode node of ast
223   */
224  const visitPropertyAndName = function (astNode): void {
225    if (!checkPropertyNeedVisit(astNode)) {
226      return;
227    }
228
229    visitChildNode(astNode);
230  };
231
232  /**
233   * commonjs exports extract
234   * examples:
235   * - exports.A = 1;
236   * - exports.B = hello; // hello can be variable or class ...
237   * - exports.C = {};
238   * - exports.D = class {};
239   * - exports.E = function () {}
240   * - class F {}
241   * - exports.F = F;
242   * - module.exports = {G: {}}
243   * - ...
244   */
245  const addCommonJsExports = function (astNode): void {
246    if (!isExpressionStatement(astNode) || !astNode.expression) {
247      return;
248    }
249
250    const expression = astNode.expression;
251    if (!isBinaryExpression(expression)) {
252      return;
253    }
254
255    const left = expression.left;
256    if (!isElementAccessExpression(left) && !isPropertyAccessExpression(left)) {
257      return;
258    }
259
260    if ((left.expression.getText() !== 'exports' && !isModuleExports(left)) ||
261      expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
262      return;
263    }
264
265    if (isElementAccessExpression(left)) {
266      if (isStringLiteral(left.argumentExpression)) {
267        mCurrentExportedPropertySet.add(left.argumentExpression.text);
268      }
269    }
270
271    if (isPropertyAccessExpression(left)) {
272      if (isIdentifier(left.name)) {
273        mCurrentExportedPropertySet.add(left.name.getText());
274      }
275    }
276
277    if (isIdentifier(expression.right)) {
278      mCurrentExportNameSet.add(expression.right.getText());
279      return;
280    }
281
282    if (isClassDeclaration(expression.right) || isClassExpression(expression.right)) {
283      getClassProperties(expression.right, mCurrentExportedPropertySet);
284      return;
285    }
286
287    if (isObjectLiteralExpression(expression.right)) {
288      getObjectProperties(expression.right, mCurrentExportedPropertySet);
289    }
290
291    return;
292  };
293
294  // module.exports = { p1: 1 }
295  function isModuleExports(astNode: Node): boolean {
296    if (isPropertyAccessExpression(astNode)) {
297      if (isIdentifier(astNode.expression) && astNode.expression.escapedText.toString() === 'module' &&
298        isIdentifier(astNode.name) && astNode.name.escapedText.toString() === 'exports') {
299        return true;
300      }
301    }
302    return false;
303  }
304
305  /**
306   * extract project export name
307   * - export {xxx, xxx};
308   * - export {xxx as xx, xxx as xx};
309   * - export default function/class/...{};
310   * - export class xxx{}
311   * - ...
312   * @param astNode
313   */
314  const visitProjectExport = function (astNode): void {
315    if (isExportAssignment(astNode)) {
316      // let xxx; export default xxx = a;
317      if (isBinaryExpression(astNode.expression)) {
318        if (isObjectLiteralExpression(astNode.expression.right)) {
319          getObjectProperties(astNode.expression.right, mCurrentExportedPropertySet);
320          return;
321        }
322
323        if (isClassExpression(astNode.expression.right)) {
324          getClassProperties(astNode.expression.right, mCurrentExportedPropertySet);
325        }
326
327        return;
328      }
329
330      // export = xxx; The xxx here can't be obfuscated
331      // export default yyy; The yyy here can be obfuscated
332      if (isIdentifier(astNode.expression)) {
333        if (!mCurrentExportNameSet.has(astNode.expression.getText())) {
334          mCurrentExportNameSet.add(astNode.expression.getText());
335          mCurrentExportedPropertySet.add(astNode.expression.getText());
336        }
337        return;
338      }
339
340      if (isObjectLiteralExpression(astNode.expression)) {
341        getObjectProperties(astNode.expression, mCurrentExportedPropertySet);
342      }
343
344      return;
345    }
346
347    if (isExportDeclaration(astNode)) {
348      if (astNode.exportClause) {
349        if (astNode.exportClause.kind === SyntaxKind.NamedExports) {
350          astNode.exportClause.forEachChild((child) => {
351            if (!isExportSpecifier(child)) {
352              return;
353            }
354
355            if (child.propertyName) {
356              mCurrentExportNameSet.add(child.propertyName.getText());
357            }
358
359            let exportName = child.name.getText();
360            mCurrentExportedPropertySet.add(exportName);
361            mCurrentExportNameSet.add(exportName);
362          });
363        }
364
365        if (astNode.exportClause.kind === SyntaxKind.NamespaceExport) {
366          mCurrentExportedPropertySet.add(astNode.exportClause.name.getText());
367          return;
368        }
369      }
370      return;
371    }
372
373    let {hasExport} = getKeyword(astNode.modifiers);
374    if (!hasExport) {
375      addCommonJsExports(astNode);
376      forEachChild(astNode, visitProjectExport);
377      return;
378    }
379
380    if (astNode.name) {
381      if (!mCurrentExportNameSet.has(astNode.name.getText())) {
382        mCurrentExportNameSet.add(astNode.name.getText());
383        mCurrentExportedPropertySet.add(astNode.name.getText());
384      }
385
386      forEachChild(astNode, visitProjectExport);
387      return;
388    }
389
390    if (isClassDeclaration(astNode)) {
391      getClassProperties(astNode, mCurrentExportedPropertySet);
392      return;
393    }
394
395    if (isVariableStatement(astNode)) {
396      astNode.declarationList.forEachChild((child) => {
397        if (isVariableDeclaration(child) && !mCurrentExportNameSet.has(child.name.getText())) {
398          mCurrentExportNameSet.add(child.name.getText());
399          mCurrentExportedPropertySet.add(child.name.getText());
400        }
401      });
402
403      return;
404    }
405
406    forEachChild(astNode, visitProjectExport);
407  };
408
409  /**
410   * extract the class, enum, and object properties of the export in the project before obfuscation
411   * class A{};
412   * export = A; need to be considered
413   * export = namespace;
414   * This statement also needs to determine whether there is an export in the namespace, and namespaces are also allowed in the namespace
415   * @param astNode
416   */
417  const visitProjectNode = function (astNode): void {
418    const currentPropsSet: Set<string> = new Set();
419    let nodeName: string | undefined = astNode.name?.text;
420    if ((isClassDeclaration(astNode) || isStructDeclaration(astNode))) {
421      getClassProperties(astNode, currentPropsSet);
422    } else if (isEnumDeclaration(astNode)) { // collect export enum structure properties
423      getEnumProperties(astNode, currentPropsSet);
424    } else if (isVariableDeclaration(astNode)) {
425      if (astNode.initializer) {
426        if (isObjectLiteralExpression(astNode.initializer)) {
427          getObjectProperties(astNode.initializer, currentPropsSet);
428        } else if (isClassExpression(astNode.initializer)) {
429          getClassProperties(astNode.initializer, currentPropsSet);
430        }
431      }
432      nodeName = astNode.name?.getText();
433    } else if (isInterfaceDeclaration(astNode)) {
434      getInterfaceProperties(astNode, currentPropsSet);
435    } else if (isTypeAliasDeclaration(astNode)) {
436      getTypeAliasProperties(astNode, currentPropsSet);
437    } else if (isElementAccessExpression(astNode)) {
438      getElementAccessExpressionProperties(astNode, currentPropsSet);
439    } else if (isObjectLiteralExpression(astNode)) {
440      getObjectProperties(astNode, currentPropsSet);
441    } else if (isClassExpression(astNode)) {
442      getClassProperties(astNode, currentPropsSet);
443    }
444
445    if (nodeName && mCurrentExportNameSet.has(nodeName)) {
446      addElement(currentPropsSet);
447    } else if (isEnumDeclaration(astNode) && scanProjectConfig.isHarCompiled) {
448      addElement(currentPropsSet);
449    }
450    forEachChild(astNode, visitProjectNode);
451  };
452
453
454  function addElement(currentPropsSet: Set<string>): void {
455    currentPropsSet.forEach((element: string) => {
456      mCurrentExportedPropertySet.add(element);
457    });
458    currentPropsSet.clear();
459  }
460  /**
461   * parse file to api list and save to json object
462   * @param fileName file name of api file
463   * @param apiType
464   * @private
465   */
466  const parseFile = function (fileName: string, apiType: ApiType): void {
467    if (!FileUtils.isReadableFile(fileName) || !isParsableFile(fileName)) {
468      return;
469    }
470
471    const sourceFile: SourceFile = createSourceFile(fileName, fs.readFileSync(fileName).toString(), ScriptTarget.ES2015, true);
472    mCurrentExportedPropertySet.clear();
473    // get export name list
474    switch (apiType) {
475      case ApiType.COMPONENT:
476        forEachChild(sourceFile, visitChildNode);
477        break;
478      case ApiType.API:
479        mCurrentExportNameSet.clear();
480        forEachChild(sourceFile, visitExport);
481
482        forEachChild(sourceFile, visitPropertyAndName);
483        mCurrentExportNameSet.clear();
484        break;
485      case ApiType.PROJECT_DEPENDS:
486      case ApiType.PROJECT:
487        mCurrentExportNameSet.clear();
488        if (fileName.endsWith('.d.ts') || fileName.endsWith('.d.ets')) {
489          forEachChild(sourceFile, visitChildNode);
490        }
491
492        forEachChild(sourceFile, visitProjectExport);
493        forEachChild(sourceFile, visitProjectNode);
494
495        if (!isRemoteHar(fileName) && scanProjectConfig.mExportObfuscation) {
496          mCurrentExportedPropertySet.clear();
497          mCurrentExportNameSet.clear();
498          return;
499        }
500
501        if (scanProjectConfig.mExportObfuscation) {
502          mCurrentExportNameSet.forEach((element) => {
503            mLibExportNameSet.add(element);
504          });
505        }
506        break;
507      case ApiType.CONSTRUCTOR_PROPERTY:
508        forEachChild(sourceFile, visitNodeForConstructorProperty);
509        break;
510      default:
511        break;
512    }
513
514    mCurrentExportNameSet.clear();
515    mCurrentExportedPropertySet.forEach(item => mPropertySet.add(item));
516    mCurrentExportedPropertySet.clear();
517  };
518
519  const projectExtensions: string[] = ['.ets', '.ts', '.js'];
520  const projectDependencyExtensions: string[] = ['.d.ets', '.d.ts', '.ets', '.ts', '.js'];
521  const resolvedModules = new Set();
522  /**
523   * traverse files of  api directory
524   * @param apiPath api directory path
525   * @param apiType
526   * @private
527   */
528  export const traverseApiFiles = function (apiPath: string, apiType: ApiType): void {
529    let fileNames: string[] = [];
530    if (fs.statSync(apiPath).isDirectory()) {
531      fileNames = fs.readdirSync(apiPath);
532      for (let fileName of fileNames) {
533        let filePath: string = path.join(apiPath, fileName);
534        try {
535          fs.accessSync(filePath, fs.constants.R_OK);
536        } catch (err) {
537          continue;
538        }
539        if (fs.statSync(filePath).isDirectory()) {
540          const ohPackageJsonPath = path.join(filePath, 'oh-package.json5');
541          let packgeNameAndVersion = '';
542          if (fs.existsSync(ohPackageJsonPath)) {
543            const ohPackageContent = json5.parse(fs.readFileSync(ohPackageJsonPath, 'utf-8'));
544            packgeNameAndVersion = ohPackageContent.name + ohPackageContent.version;
545            if (resolvedModules.has(packgeNameAndVersion)) {
546              continue;
547            }
548          }
549          traverseApiFiles(filePath, apiType);
550          packgeNameAndVersion.length > 0 && resolvedModules.add(packgeNameAndVersion);
551          continue;
552        }
553        const suffix: string = path.extname(filePath);
554        if ((apiType !== ApiType.PROJECT) && !projectDependencyExtensions.includes(suffix)) {
555          continue;
556        }
557
558        if (apiType === ApiType.PROJECT && !projectExtensions.includes(suffix)) {
559          continue;
560        }
561        parseFile(filePath, apiType);
562      }
563    } else {
564      parseFile(apiPath, apiType);
565    }
566  };
567
568  /**
569   * desc: parse openHarmony sdk to get api list
570   * @param version version of api, e.g. version 5.0.1.0 for api 9
571   * @param sdkPath sdk real path of openHarmony
572   * @param isEts true for ets, false for js
573   * @param outputDir: sdk api output directory
574   */
575  export function parseOhSdk(sdkPath: string, version: string, isEts: boolean, outputDir: string): void {
576    mPropertySet.clear();
577
578    // visit api directory
579    const apiPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version, 'api');
580    traverseApiFiles(apiPath, ApiType.API);
581
582    // visit component directory if ets
583    if (isEts) {
584      const componentPath: string = path.join(sdkPath, 'ets', version, 'component');
585      traverseApiFiles(componentPath, ApiType.COMPONENT);
586    }
587
588    // visit the UI conversion API
589    const uiConversionPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version,
590      'build-tools', 'ets-loader', 'lib', 'pre_define.js');
591    extractStringsFromFile(uiConversionPath);
592
593    const reservedProperties: string[] = [...mPropertySet.values()];
594    mPropertySet.clear();
595
596    writeToFile(reservedProperties, path.join(outputDir, 'propertiesReserved.json'));
597  }
598
599  export function extractStringsFromFile(filePath: string): void {
600    let collections: string[] = [];
601    const fileContent = fs.readFileSync(filePath, 'utf-8');
602    const regex = /"([^"]*)"/g;
603    const matches = fileContent.match(regex);
604
605    if (matches) {
606      collections = matches.map(match => match.slice(1, -1));
607    }
608
609    collections.forEach(name => mPropertySet.add(name));
610  }
611
612  /**
613   * parse common project or file to extract exported api list
614   * @return reserved api names
615   */
616  export function parseCommonProject(projectPath: string, customProfiles: IOptions, scanningApiType: ApiType): string[] {
617    mPropertySet.clear();
618    stringPropsSet.clear();
619    if (fs.lstatSync(projectPath).isFile()) {
620      if (projectPath.endsWith('.ets') || projectPath.endsWith('.ts') || projectPath.endsWith('.js')) {
621        parseFile(projectPath, scanningApiType);
622      }
623    } else {
624      traverseApiFiles(projectPath, scanningApiType);
625    }
626
627    let reservedProperties: string[] = customProfiles.mExportObfuscation ? [] : [...mPropertySet];
628    mPropertySet.clear();
629    return reservedProperties;
630  }
631
632  /**
633   * parse api of third party libs like libs in node_modules
634   * @param libPath
635   */
636  export function parseThirdPartyLibs(libPath: string, scanningApiType: ApiType): {reservedProperties: string[]; reservedLibExportNames: string[] | undefined} {
637    mPropertySet.clear();
638    mLibExportNameSet.clear();
639    stringPropsSet.clear();
640    if (fs.lstatSync(libPath).isFile()) {
641      if (libPath.endsWith('.ets') || libPath.endsWith('.ts') || libPath.endsWith('.js')) {
642        parseFile(libPath, scanningApiType);
643      }
644    } else {
645      const filesAndfolders = fs.readdirSync(libPath);
646      for (let subPath of filesAndfolders) {
647        traverseApiFiles(path.join(libPath, subPath), scanningApiType);
648      }
649    }
650    let reservedLibExportNames: string[] = undefined;
651    if (scanProjectConfig.mExportObfuscation) {
652      reservedLibExportNames = [...mLibExportNameSet];
653      mLibExportNameSet.clear();
654    }
655    const reservedProperties: string[] = [...mPropertySet];
656    mPropertySet.clear();
657
658    return {reservedProperties: reservedProperties, reservedLibExportNames: reservedLibExportNames};
659  }
660
661  /**
662   * save api json object to file
663   * @private
664   */
665  export function writeToFile(reservedProperties: string[], outputPath: string): void {
666    let str: string = JSON.stringify(reservedProperties, null, '\t');
667    fs.writeFileSync(outputPath, str);
668  }
669
670  export function isRemoteHar(filePath: string): boolean {
671    const realPath: string = sys.realpath(filePath);
672    return isInOhModuleFile(realPath);
673  }
674
675  export function isInOhModuleFile(filePath: string): boolean {
676    return filePath.indexOf('/oh_modules/') !== -1 || filePath.indexOf('\\oh_modules\\') !== -1;
677  }
678
679  export function isParsableFile(path: string): boolean {
680    return supportedParsingExtension.some(extension => path.endsWith(extension));
681  }
682
683   /**
684   * parse common project or file to extract exported api list
685   * @return reserved api names
686   */
687   export function parseProjectSourceByPaths(projectPaths: string[], customProfiles: IOptions, scanningApiType: ApiType): string[] {
688    mPropertySet.clear();
689    projectPaths.forEach(path => {
690      parseFile(path, scanningApiType);
691    })
692    let reservedProperties: string[] = customProfiles.mExportObfuscation ? [] : [...mPropertySet];
693    mPropertySet.clear();
694    return reservedProperties;
695  }
696
697  /**
698   * parse api of third party libs like libs in node_modules
699   * @param libPath
700   */
701  export function parseThirdPartyLibsByPaths(libPaths: string[], scanningApiType: ApiType): {reservedProperties: string[]; reservedLibExportNames: string[] | undefined} {
702    mPropertySet.clear();
703    mLibExportNameSet.clear();
704    libPaths.forEach(path => {
705      parseFile(path, scanningApiType);
706    })
707    let reservedLibExportNames: string[] = undefined;
708    if (scanProjectConfig.mExportObfuscation) {
709      reservedLibExportNames = [...mLibExportNameSet];
710      mLibExportNameSet.clear();
711    }
712    const reservedProperties: string[] = [...mPropertySet];
713    mPropertySet.clear();
714
715    return {reservedProperties: reservedProperties, reservedLibExportNames: reservedLibExportNames};
716  }
717}
718