• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 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 { describe, it } from 'mocha';
17import { assert, expect } from 'chai';
18import { IOptions } from '../../../src/configs/IOptions';
19import { FileUtils } from '../../../src/utils/FileUtils';
20import { ArkObfuscatorForTest } from '../../../src/ArkObfuscatorForTest'
21import secharmony, {
22  transformerPlugin,
23  historyNameCache,
24  clearCaches,
25 } from '../../../src/transformers/rename/RenameIdentifierTransformer';
26import path from 'path';
27import * as ts from 'typescript';
28import {
29  PropCollections,
30  UnobfuscationCollections,
31  LocalVariableCollections
32} from '../../../src/utils/CommonCollections';
33
34describe('Teste Cases for <RenameFileNameTransformer>.', function () {
35  describe('Teste Cases for <createRenameIdentifierFactory>.', function () {
36    it('should return null if mEnable is false',function () {
37      let options: IOptions = {
38        "mNameObfuscation": {
39          "mEnable": false,
40          "mRenameProperties": false,
41          "mReservedProperties": []
42        }
43      };
44      let renameIdentifierFactory = transformerPlugin.createTransformerFactory(options);
45      expect(renameIdentifierFactory).to.be.null;
46    })
47    describe('Teste Cases for <renameTransformer>.', function () {
48      let option: IOptions = {
49        "mCompact": false,
50        "mRemoveComments": false,
51        "mOutputDir": "",
52        "mDisableConsole": false,
53        "mSimplify": false,
54        "mNameObfuscation": {
55            "mEnable": true,
56            "mNameGeneratorType": 1,
57            "mDictionaryList": [],
58            "mRenameProperties": true,
59            "mKeepStringProperty": false,
60            "mTopLevel": false,
61            "mReservedProperties": []
62        },
63        "mEnableSourceMap": true,
64        "mEnableNameCache": true
65      };
66      const fileContent = `
67      class A1{
68        prop_5 = 5;
69        constructor(public para1: number, private para2: string, protected para3: boolean, readonly para4: number, para5: string) {
70          para5 = para5 + 1;
71          let temp1 = para1;
72          let temp2 = para2;
73          let temp3 = para3;
74          let temp4 = para4;
75          this.prop_5 = para4;
76        }
77      }
78      `;
79      const fileContent1 = `
80      class a{
81        prop_5 = 5;
82        constructor(public para1: number, private para2: string, protected para3: boolean, readonly para4: number, para5: string) {
83          para5 = para5 + 1;
84          let temp1 = para1;
85          let temp2 = para2;
86          let temp3 = para3;
87          let temp4 = para4;
88          this.prop_5 = para4;
89        }
90      }
91      `;
92      const fileContent2 = `
93      import {A as B} from './file1.ts';
94      export {C as D} from './file1.ts';
95      `;
96      let transformer: ts.TransformerFactory<ts.Node>;
97
98      it('should not transform parameter property when mRenameProperties is false',function () {
99        let option1: IOptions = {
100          "mCompact": false,
101          "mRemoveComments": false,
102          "mOutputDir": "",
103          "mDisableConsole": false,
104          "mSimplify": false,
105          "mNameObfuscation": {
106              "mEnable": true,
107              "mNameGeneratorType": 1,
108              "mDictionaryList": [],
109              "mRenameProperties": false,
110              "mKeepStringProperty": false,
111              "mTopLevel": false,
112              "mReservedProperties": []
113          },
114          "mEnableSourceMap": true,
115          "mEnableNameCache": true
116        };
117        transformer = transformerPlugin.createTransformerFactory(option1);
118        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
119        let transformed = ts.transform(sourceFile, [transformer]);
120        expect(
121          ((((transformed
122            .transformed[0] as ts.SourceFile)
123            .statements[0] as ts.ClassDeclaration)
124            .members[1] as ts.ConstructorDeclaration)
125            .parameters[0]
126            .name as ts.Identifier)
127            .escapedText == 'para1')
128            .to.be.true;
129        expect(
130          ((((transformed
131            .transformed[0] as ts.SourceFile)
132            .statements[0] as ts.ClassDeclaration)
133            .members[1] as ts.ConstructorDeclaration)
134            .parameters[1]
135            .name as ts.Identifier)
136            .escapedText == 'para2')
137            .to.be.true;
138        expect(
139          ((((transformed
140            .transformed[0] as ts.SourceFile)
141            .statements[0] as ts.ClassDeclaration)
142            .members[1] as ts.ConstructorDeclaration)
143            .parameters[2]
144            .name as ts.Identifier)
145            .escapedText == 'para3')
146            .to.be.true;
147        expect(
148          ((((transformed
149            .transformed[0] as ts.SourceFile)
150            .statements[0] as ts.ClassDeclaration)
151            .members[1] as ts.ConstructorDeclaration)
152            .parameters[3]
153            .name as ts.Identifier)
154            .escapedText == 'para4')
155            .to.be.true;
156        expect(
157          ((((transformed
158            .transformed[0] as ts.SourceFile)
159            .statements[0] as ts.ClassDeclaration)
160            .members[1] as ts.ConstructorDeclaration)
161            .parameters[4]
162            .name as ts.Identifier)
163            .escapedText == 'a')
164            .to.be.true;
165      })
166
167      it('should not transform parameter property when mRenameProperties is true',function () {
168        transformer = transformerPlugin.createTransformerFactory(option);
169        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
170        let transformed = ts.transform(sourceFile, [transformer]);
171        expect(
172          ((((transformed
173            .transformed[0] as ts.SourceFile)
174            .statements[0] as ts.ClassDeclaration)
175            .members[1] as ts.ConstructorDeclaration)
176            .parameters[0].name as ts.Identifier)
177            .escapedText == 'a')
178            .to.be.true;
179        expect(
180          ((((transformed
181            .transformed[0] as ts.SourceFile)
182            .statements[0] as ts.ClassDeclaration)
183            .members[1] as ts.ConstructorDeclaration)
184            .parameters[1]
185            .name as ts.Identifier)
186            .escapedText == 'b')
187            .to.be.true;
188        expect(
189          ((((transformed
190            .transformed[0] as ts.SourceFile)
191            .statements[0] as ts.ClassDeclaration)
192            .members[1] as ts.ConstructorDeclaration)
193            .parameters[2]
194            .name as ts.Identifier)
195            .escapedText == 'c')
196            .to.be.true;
197        expect(
198          ((((transformed
199            .transformed[0] as ts.SourceFile)
200            .statements[0] as ts.ClassDeclaration)
201            .members[1] as ts.ConstructorDeclaration)
202            .parameters[3]
203            .name as ts.Identifier)
204            .escapedText == 'd')
205            .to.be.true;
206        expect(
207          ((((transformed
208            .transformed[0] as ts.SourceFile)
209            .statements[0] as ts.ClassDeclaration)
210            .members[1] as ts.ConstructorDeclaration)
211            .parameters[4]
212            .name as ts.Identifier)
213            .escapedText == 'e')
214            .to.be.true;
215      })
216
217      it('should use historyMangledName when originName is in historyMangledTable', function () {
218        PropCollections.historyMangledTable.set('para1', 'test1');
219        PropCollections.globalMangledTable.set('para1', 'test2');
220        transformer = transformerPlugin.createTransformerFactory(option);
221        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
222        let transformed = ts.transform(sourceFile, [transformer]);
223        expect(
224          ((((transformed
225            .transformed[0] as ts.SourceFile)
226            .statements[0] as ts.ClassDeclaration)
227            .members[1] as ts.ConstructorDeclaration)
228            .parameters[0]
229            .name as ts.Identifier)
230            .escapedText == 'test1')
231            .to.be.true;
232        PropCollections.historyMangledTable.clear();
233        PropCollections.globalMangledTable.clear();
234      })
235
236      it('should use historyMangledName when originName is in globalMangleTable', function () {
237        PropCollections.globalMangledTable.set('para1', 'test2');
238        transformer = transformerPlugin.createTransformerFactory(option);
239        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
240        let transformed = ts.transform(sourceFile, [transformer]);
241        expect(
242          ((((transformed
243            .transformed[0] as ts.SourceFile)
244            .statements[0] as ts.ClassDeclaration)
245            .members[1] as ts.ConstructorDeclaration)
246            .parameters[0]
247            .name as ts.Identifier)
248            .escapedText == 'test2')
249            .to.be.true;
250        PropCollections.globalMangledTable.clear();
251      })
252
253      it('should not obfuscate when originName is in property whitelist', function () {
254        PropCollections.reservedProperties.add('para1');
255        transformer = transformerPlugin.createTransformerFactory(option);
256        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
257        let transformed = ts.transform(sourceFile, [transformer]);
258        expect(
259          ((((transformed
260            .transformed[0] as ts.SourceFile)
261            .statements[0] as ts.ClassDeclaration)
262            .members[1] as ts.ConstructorDeclaration)
263            .parameters[0].name as ts.Identifier)
264            .escapedText == 'para1')
265            .to.be.true;
266        PropCollections.reservedProperties.clear();
267        PropCollections.globalMangledTable.clear();
268      })
269
270      it('should not obfuscated as names in ReservedProperty or ReservedLocalVariable or mangledPropsInNameCache', function () {
271        PropCollections.reservedProperties.add('b');
272        UnobfuscationCollections.reservedExportName.add('c');
273        PropCollections.historyMangledTable.set('testorigin', 'd');
274        transformer = transformerPlugin.createTransformerFactory(option);
275        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
276        let transformed = ts.transform(sourceFile, [transformer]);
277        expect(
278          ((((transformed
279            .transformed[0] as ts.SourceFile)
280            .statements[0] as ts.ClassDeclaration)
281            .members[1] as ts.ConstructorDeclaration)
282            .parameters[0]
283            .name as ts.Identifier)
284            .escapedText == 'a')
285            .to.be.true;
286        expect(
287          ((((transformed
288            .transformed[0] as ts.SourceFile)
289            .statements[0] as ts.ClassDeclaration)
290            .members[1] as ts.ConstructorDeclaration)
291            .parameters[1]
292            .name as ts.Identifier)
293            .escapedText == 'e')
294            .to.be.true;
295        expect(
296          ((((transformed
297            .transformed[0] as ts.SourceFile)
298            .statements[0] as ts.ClassDeclaration)
299            .members[1] as ts.ConstructorDeclaration)
300            .parameters[2]
301            .name as ts.Identifier)
302            .escapedText == 'f')
303            .to.be.true;
304        expect(
305          ((((transformed
306            .transformed[0] as ts.SourceFile)
307            .statements[0] as ts.ClassDeclaration)
308            .members[1] as ts.ConstructorDeclaration)
309            .parameters[3]
310            .name as ts.Identifier)
311            .escapedText == 'g')
312            .to.be.true;
313        expect(
314          ((((transformed
315            .transformed[0] as ts.SourceFile)
316            .statements[0] as ts.ClassDeclaration)
317            .members[1] as ts.ConstructorDeclaration)
318            .parameters[4]
319            .name as ts.Identifier)
320            .escapedText == 'h')
321            .to.be.true;
322        PropCollections.reservedProperties.clear();
323        UnobfuscationCollections.reservedExportName.clear();
324        PropCollections.globalMangledTable.clear();
325        UnobfuscationCollections.reservedExportNameAndProp.clear();
326        PropCollections.historyMangledTable.clear();
327      })
328
329      it('should not obfuscated as names in outer scope', function () {
330        transformer = transformerPlugin.createTransformerFactory(option);
331        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent1, ts.ScriptTarget.ES2015, true);
332        let transformed = ts.transform(sourceFile, [transformer]);
333        expect(
334          ((((transformed
335            .transformed[0] as ts.SourceFile)
336            .statements[0] as ts.ClassDeclaration)
337            .members[1] as ts.ConstructorDeclaration)
338            .parameters[0]
339            .name as ts.Identifier)
340            .escapedText == 'b')
341            .to.be.true;
342        expect(
343          ((((transformed
344            .transformed[0] as ts.SourceFile)
345            .statements[0] as ts.ClassDeclaration)
346            .members[1] as ts.ConstructorDeclaration)
347            .parameters[1]
348            .name as ts.Identifier)
349            .escapedText == 'c')
350            .to.be.true;
351        expect(
352          ((((transformed
353            .transformed[0] as ts.SourceFile)
354            .statements[0] as ts.ClassDeclaration)
355            .members[1] as ts.ConstructorDeclaration)
356            .parameters[2]
357            .name as ts.Identifier)
358            .escapedText == 'd')
359            .to.be.true;
360        expect(
361          ((((transformed
362            .transformed[0] as ts.SourceFile)
363            .statements[0] as ts.ClassDeclaration)
364            .members[1] as ts.ConstructorDeclaration)
365            .parameters[3]
366            .name as ts.Identifier)
367            .escapedText == 'e')
368            .to.be.true;
369        expect(
370          ((((transformed
371            .transformed[0] as ts.SourceFile)
372            .statements[0] as ts.ClassDeclaration)
373            .members[1] as ts.ConstructorDeclaration)
374            .parameters[4]
375            .name as ts.Identifier)
376            .escapedText == 'f')
377            .to.be.true;
378        PropCollections.globalMangledTable.clear();
379      })
380
381      it('Only Enable Toplevel Obfuscation Test', () => {
382        let options: IOptions = {
383          "mNameObfuscation": {
384            "mEnable": true,
385            "mRenameProperties": false,
386            "mReservedProperties": [],
387            "mTopLevel": true
388          }
389        };
390        assert.strictEqual(options !== undefined, true);
391        const renameIdentifierFactory = secharmony.transformerPlugin.createTransformerFactory(options);
392        const fileContent = `
393          let a = 1;
394          export let b = 1;
395          import {c} from 'filePath';
396        `;
397        const textWriter = ts.createTextWriter('\n');
398        let arkobfuscator = new ArkObfuscatorForTest();
399        arkobfuscator.init(options);
400        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent, ts.ScriptTarget.ES2015, true);
401        let transformedResult: ts.TransformationResult<ts.Node> = ts.transform(sourceFile, [renameIdentifierFactory], {});
402        let ast: ts.SourceFile = transformedResult.transformed[0] as ts.SourceFile;
403        arkobfuscator.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, textWriter, undefined);
404        const actualContent = textWriter.getText();
405        const expectContent = `
406          let d = 1;
407          export let b = 1;
408          import {c} from 'filePath';
409        `;
410        assert.strictEqual(compareStringsIgnoreNewlines(actualContent, expectContent), true);
411      })
412
413      it('should return origin node if isSourceFile is false', () => {
414        let options: IOptions | undefined = FileUtils.readFileAsJson(path.join(__dirname, "obfuscate_identifier_config.json"));
415        assert.strictEqual(options !== undefined, true);
416        const renameIdentifierFactory = secharmony.transformerPlugin.createTransformerFactory(options as IOptions);
417        const blockFile: ts.Block = ts.factory.createBlock([]);
418        let transformedResult: ts.TransformationResult<ts.Node> = ts.transform(blockFile, [renameIdentifierFactory], {});
419        assert.strictEqual(transformedResult.transformed[0], blockFile);
420      })
421
422      it('noSymbolIdentifierTest: enable export obfuscation', function () {
423        let option1: IOptions = {
424          mCompact: false,
425          mRemoveComments: false,
426          mOutputDir: '',
427          mDisableConsole: false,
428          mSimplify: false,
429          mNameObfuscation: {
430            mEnable: true,
431            mNameGeneratorType: 1,
432            mDictionaryList: [],
433            mRenameProperties: false,
434            mKeepStringProperty: false,
435            mTopLevel: false,
436            mReservedProperties: [],
437          },
438          mExportObfuscation: true,
439          mEnableSourceMap: false,
440          mEnableNameCache: false,
441        };
442        transformer = transformerPlugin.createTransformerFactory(option1);
443        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent2, ts.ScriptTarget.ES2015, true);
444        let transformed = ts.transform(sourceFile, [transformer]);
445        let ast: ts.SourceFile = transformed.transformed[0] as ts.SourceFile;
446        const printer = ts.createPrinter();
447        const transformedAst: string = printer.printFile(ast);
448        expect(transformedAst === "import { A as B } from './file1.ts';\nexport { C as D } from './file1.ts';\n").to.be
449          .true;
450      });
451
452      it('noSymbolIdentifierTest: enable export and toplevel obfuscation', function () {
453        let option1: IOptions = {
454          mCompact: false,
455          mRemoveComments: false,
456          mOutputDir: '',
457          mDisableConsole: false,
458          mSimplify: false,
459          mNameObfuscation: {
460            mEnable: true,
461            mNameGeneratorType: 1,
462            mDictionaryList: [],
463            mRenameProperties: false,
464            mKeepStringProperty: false,
465            mTopLevel: true,
466            mReservedProperties: [],
467          },
468          mExportObfuscation: true,
469          mEnableSourceMap: false,
470          mEnableNameCache: false,
471        };
472        transformer = transformerPlugin.createTransformerFactory(option1);
473        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent2, ts.ScriptTarget.ES2015, true);
474        let transformed = ts.transform(sourceFile, [transformer]);
475        let ast: ts.SourceFile = transformed.transformed[0] as ts.SourceFile;
476        const printer = ts.createPrinter();
477        const transformedAst: string = printer.printFile(ast);
478        expect(transformedAst === "import { c as a } from './file1.ts';\nexport { d as b } from './file1.ts';\n").to.be
479          .true;
480      });
481
482      it('originalSymbolTest: import test', function () {
483        const fileContent3 = `
484          declare module 'testModule2' {
485            import { noSymbolIdentifier2 as ni2 } from 'module2';
486            export { ni2 };
487          }
488        `;
489        let option: IOptions = {
490          mCompact: false,
491          mRemoveComments: false,
492          mOutputDir: '',
493          mDisableConsole: false,
494          mSimplify: false,
495          mNameObfuscation: {
496            mEnable: true,
497            mNameGeneratorType: 1,
498            mDictionaryList: [],
499            mRenameProperties: false,
500            mKeepStringProperty: false,
501            mTopLevel: true,
502            mReservedProperties: [],
503          },
504          mExportObfuscation: true,
505          mEnableSourceMap: false,
506          mEnableNameCache: false
507        };
508        transformer = transformerPlugin.createTransformerFactory(option);
509        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent3, ts.ScriptTarget.ES2015, true);
510        let transformed = ts.transform(sourceFile, [transformer]);
511        let ast: ts.SourceFile = transformed.transformed[0] as ts.SourceFile;
512        const printer = ts.createPrinter();
513        const transformedAst: string = printer.printFile(ast);
514        expect(
515          transformedAst ===
516            "declare module 'testModule2' {\n    import { c as b } from 'module2';\n    export { b };\n}\n",
517        ).to.be.true;
518      });
519
520      it('originalSymbolTest: export test', function () {
521        const fileContent4 = `
522          type ni2 = string;
523          declare namespace ns {
524            export { ni2 };
525          }
526        `;
527        let option: IOptions = {
528          mCompact: false,
529          mRemoveComments: false,
530          mOutputDir: '',
531          mDisableConsole: false,
532          mSimplify: false,
533          mNameObfuscation: {
534            mEnable: true,
535            mNameGeneratorType: 1,
536            mDictionaryList: [],
537            mRenameProperties: false,
538            mKeepStringProperty: false,
539            mTopLevel: true,
540            mReservedProperties: [],
541          },
542          mExportObfuscation: true,
543          mEnableSourceMap: false,
544          mEnableNameCache: false
545        };
546        transformer = transformerPlugin.createTransformerFactory(option);
547        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent4, ts.ScriptTarget.ES2015, true);
548        let transformed = ts.transform(sourceFile, [transformer]);
549        let ast: ts.SourceFile = transformed.transformed[0] as ts.SourceFile;
550        const printer = ts.createPrinter();
551        const transformedAst: string = printer.printFile(ast);
552        expect(
553          transformedAst ===
554            "type b = string;\ndeclare namespace a {\n    export { b };\n}\n",
555        ).to.be.true;
556      });
557
558      it('originalSymbolTest: propertyName test', function () {
559        const fileContent5 = `
560          import { Symbol as sy } from 'typescript';
561          let localVariable: number = 1;
562          export { SourceFile sf } from 'typescript';
563          export { localVariable as lv };
564        `;
565        let option: IOptions = {
566          mCompact: false,
567          mRemoveComments: false,
568          mOutputDir: '',
569          mDisableConsole: false,
570          mSimplify: false,
571          mNameObfuscation: {
572            mEnable: true,
573            mNameGeneratorType: 1,
574            mDictionaryList: [],
575            mRenameProperties: false,
576            mKeepStringProperty: false,
577            mTopLevel: true,
578            mReservedProperties: [],
579          },
580          mExportObfuscation: true,
581          mEnableSourceMap: false,
582          mEnableNameCache: false,
583        };
584        transformer = transformerPlugin.createTransformerFactory(option);
585        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.ts', fileContent5, ts.ScriptTarget.ES2015, true);
586        let transformed = ts.transform(sourceFile, [transformer]);
587        let ast: ts.SourceFile = transformed.transformed[0] as ts.SourceFile;
588        const printer = ts.createPrinter();
589        const transformedAst: string = printer.printFile(ast);
590        expect(
591          transformedAst ===
592            "import { f as a } from 'typescript';\nlet b: number = 1;\nexport { c, d } from 'typescript';\nexport { b as e };\n",
593        ).to.be.true;
594      });
595
596      it('Test the option of mKeepParameterNames for declaration file', () => {
597        let options: IOptions = {
598          'mNameObfuscation': {
599            'mEnable': true,
600            'mRenameProperties': false,
601            'mReservedProperties': [],
602            'mTopLevel': false,
603            'mKeepParameterNames': true
604          }
605        };
606        assert.strictEqual(options !== undefined, true);
607        const renameIdentifierFactory = secharmony.transformerPlugin.createTransformerFactory(options);
608        const fileContent = `export declare function foo(para: number): void;`;
609        const textWriter = ts.createTextWriter('\n');
610        let arkobfuscator = new ArkObfuscatorForTest();
611        arkobfuscator.init(options);
612        const sourceFile: ts.SourceFile = ts.createSourceFile('demo.d.ts', fileContent, ts.ScriptTarget.ES2015, true);
613        let transformedResult: ts.TransformationResult<ts.Node> = ts.transform(sourceFile, [renameIdentifierFactory], {});
614        let ast: ts.SourceFile = transformedResult.transformed[0] as ts.SourceFile;
615        arkobfuscator.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, textWriter, undefined);
616        const actualContent = textWriter.getText();
617        const expectContent = `export declare function foo(para: number): void;`;
618        assert.strictEqual(compareStringsIgnoreNewlines(actualContent, expectContent), true);
619      })
620    })
621  })
622})
623
624function compareStringsIgnoreNewlines(str1: string, str2: string): boolean {
625  const normalize = (str: string) => str.replace(/[\n\r\s]+/g, '');
626  return normalize(str1) === normalize(str2);
627}