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}