소스 검색

`splitString` and `splitStringChunkLength` options

sanex3339 5 년 전
부모
커밋
783067b505
24개의 변경된 파일393개의 추가작업 그리고 4개의 파일을 삭제
  1. 5 0
      CHANGELOG.md
  2. 32 0
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 1 1
      package.json
  7. 1 0
      src/JavaScriptObfuscator.ts
  8. 10 0
      src/cli/JavaScriptObfuscatorCLI.ts
  9. 5 0
      src/container/modules/node-transformers/ConvertingTransformersModule.ts
  10. 1 0
      src/enums/node-transformers/NodeTransformer.ts
  11. 2 0
      src/interfaces/options/IOptions.d.ts
  12. 127 0
      src/node-transformers/converting-transformers/SplitStringTransformer.ts
  13. 14 0
      src/options/Options.ts
  14. 2 0
      src/options/OptionsNormalizer.ts
  15. 24 0
      src/options/normalizer-rules/SplitStringsChunkLengthRule.ts
  16. 2 0
      src/options/presets/Default.ts
  17. 2 0
      src/options/presets/NoCustomNodes.ts
  18. 5 3
      test/dev/dev.ts
  19. 130 0
      test/functional-tests/node-transformers/converting-transformers/split-string-transformer/SplitStringTransformer.spec.ts
  20. 3 0
      test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/object-computed-key-string-literal.js
  21. 3 0
      test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/object-key-string-literal.js
  22. 1 0
      test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/simple-input.js
  23. 1 0
      test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/strings-concatenation.js
  24. 22 0
      test/functional-tests/options/OptionsNormalizer.spec.ts

+ 5 - 0
CHANGELOG.md

@@ -1,5 +1,10 @@
 Change Log
 
+v0.19.0
+---
+* **New option:** `splitStrings` splits literal strings into chunks with length of `splitStringsChunkLength` option value
+* **New option:** `splitStringsChunkLength` sets chunk length of `splitStrings` option
+
 v0.18.8
 ---
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/452

+ 32 - 0
README.md

@@ -307,6 +307,8 @@ Following options are available for the JS Obfuscator:
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapMode: 'separate',
+    splitStrings: false,
+    splitStringsChunkLength: 10,
     stringArray: true,
     stringArrayEncoding: false,
     stringArrayThreshold: 0.75,
@@ -347,6 +349,8 @@ Following options are available for the JS Obfuscator:
     --source-map-base-url <string>
     --source-map-file-name <string>
     --source-map-mode <string> [inline, separate]
+    --split-strings <boolean>
+    --split-strings-chunk-length <number>
     --string-array <boolean>
     --string-array-encoding <boolean|string> [true, false, base64, rc4]
     --string-array-threshold <number>
@@ -721,6 +725,29 @@ Specifies source map generation mode:
 * `inline` - emit a single file with source maps instead of having a separate file;
 * `separate` - generates corresponding '.map' file with source map. In case you run obfuscator through CLI - adds link to source map file to the end of file with obfuscated code `//# sourceMappingUrl=file.js.map`.
 
+### `splitStrings`
+Type: `boolean` Default: `false`
+
+Splits literal strings into chunks with length of [`splitStringsChunkLength`](#splitStringsChunkLength) option value.
+
+Example:
+```ts
+// input
+(function(){
+    var test = 'abcdefg';
+})();
+
+// output
+(function(){
+    var _0x5a21 = 'ab' + 'cd' + 'ef' + 'g';
+})();
+```
+
+### `splitStringsChunkLength`
+Type: `number` Default: `10`
+
+Sets chunk length of [`splitStrings`](#splitStrings) option.
+
 ### `stringArray`
 Type: `boolean` Default: `true`
 
@@ -830,6 +857,8 @@ Performance will 50-100% slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    splitStrings: true,
+    splitStringsChunkLength: '5',
     stringArray: true,
     stringArrayEncoding: 'rc4',
     stringArrayThreshold: 1,
@@ -857,6 +886,8 @@ Performance will 30-35% slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    splitStrings: true,
+    splitStringsChunkLength: '10',
     stringArray: true,
     stringArrayEncoding: 'base64',
     stringArrayThreshold: 0.75,
@@ -882,6 +913,7 @@ Performance will slightly slower than without obfuscation
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
+    splitStrings: false,
     stringArray: true,
     stringArrayEncoding: false,
     stringArrayThreshold: 0.75,

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.browser.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.cli.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.js


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "javascript-obfuscator",
-  "version": "0.18.8",
+  "version": "0.19.0",
   "description": "JavaScript obfuscator",
   "keywords": [
     "obfuscator",

+ 1 - 0
src/JavaScriptObfuscator.ts

@@ -67,6 +67,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator {
         NodeTransformer.ObjectExpressionKeysTransformer,
         NodeTransformer.ObjectExpressionTransformer,
         NodeTransformer.ParentificationTransformer,
+        NodeTransformer.SplitStringTransformer,
         NodeTransformer.TemplateLiteralTransformer,
         NodeTransformer.VariableDeclarationTransformer,
         NodeTransformer.VariablePreserveTransformer

+ 10 - 0
src/cli/JavaScriptObfuscatorCLI.ts

@@ -311,6 +311,16 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 'Default: separate',
                 SourceMapModeSanitizer
             )
+            .option(
+                '--split-strings <boolean>',
+                'Splits literal strings into chunks with length of `splitStringsChunkLength` option value',
+                BooleanSanitizer
+            )
+            .option(
+                '--split-strings-chunk-length <number>',
+                'Sets chunk length of `splitStrings` option',
+                parseFloat
+            )
             .option(
                 '--string-array <boolean>',
                 'Disables gathering of all literal strings into an array and replacing every literal string with an array call',

+ 5 - 0
src/container/modules/node-transformers/ConvertingTransformersModule.ts

@@ -13,6 +13,7 @@ import { MemberExpressionTransformer } from '../../../node-transformers/converti
 import { MethodDefinitionTransformer } from '../../../node-transformers/converting-transformers/MethodDefinitionTransformer';
 import { ObjectExpressionKeysTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionKeysTransformer';
 import { ObjectExpressionTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionTransformer';
+import { SplitStringTransformer } from '../../../node-transformers/converting-transformers/SplitStringTransformer';
 import { TemplateLiteralTransformer } from '../../../node-transformers/converting-transformers/TemplateLiteralTransformer';
 import { VariableDeclaratorPropertiesExtractor } from '../../../node-transformers/converting-transformers/properties-extractors/VariableDeclaratorPropertiesExtractor';
 
@@ -34,6 +35,10 @@ export const convertingTransformersModule: interfaces.ContainerModule = new Cont
         .to(ObjectExpressionTransformer)
         .whenTargetNamed(NodeTransformer.ObjectExpressionTransformer);
 
+    bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
+        .to(SplitStringTransformer)
+        .whenTargetNamed(NodeTransformer.SplitStringTransformer);
+
     bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
         .to(TemplateLiteralTransformer)
         .whenTargetNamed(NodeTransformer.TemplateLiteralTransformer);

+ 1 - 0
src/enums/node-transformers/NodeTransformer.ts

@@ -19,6 +19,7 @@ export enum NodeTransformer {
     ObjectExpressionKeysTransformer = 'ObjectExpressionKeysTransformer',
     ObjectExpressionTransformer = 'ObjectExpressionTransformer',
     ParentificationTransformer = 'ParentificationTransformer',
+    SplitStringTransformer = 'SplitStringTransformer',
     TemplateLiteralTransformer = 'TemplateLiteralTransformer',
     VariableDeclarationTransformer = 'VariableDeclarationTransformer',
     VariablePreserveTransformer = 'VariablePreserveTransformer',

+ 2 - 0
src/interfaces/options/IOptions.d.ts

@@ -28,6 +28,8 @@ export interface IOptions {
     readonly sourceMapBaseUrl: string;
     readonly sourceMapFileName: string;
     readonly sourceMapMode: SourceMapMode;
+    readonly splitStrings: boolean;
+    readonly splitStringsChunkLength: number;
     readonly stringArray: boolean;
     readonly stringArrayEncoding: TStringArrayEncoding;
     readonly stringArrayThreshold: number;

+ 127 - 0
src/node-transformers/converting-transformers/SplitStringTransformer.ts

@@ -0,0 +1,127 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
+
+import { TransformationStage } from '../../enums/node-transformers/TransformationStage';
+
+import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NodeGuards } from '../../node/NodeGuards';
+
+/**
+ * Splits strings into parts
+ */
+@injectable()
+export class SplitStringTransformer extends AbstractNodeTransformer {
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(randomGenerator, options);
+    }
+
+    /**
+     * @param {string} string
+     * @param {number} chunkSize
+     * @returns {string[]}
+     */
+    private static chunkString (string: string, chunkSize: number): string[] {
+        const chunksCount: number = Math.ceil(string.length / chunkSize);
+        const chunks: string[] = [];
+
+        let nextChunkStartIndex: number = 0;
+
+        for (
+            let chunkIndex: number = 0;
+            chunkIndex < chunksCount;
+            ++chunkIndex, nextChunkStartIndex += chunkSize
+        ) {
+            chunks[chunkIndex] = string.substr(nextChunkStartIndex, chunkSize);
+        }
+
+        return chunks;
+    }
+
+    /**
+     * @param {TransformationStage} transformationStage
+     * @returns {IVisitor | null}
+     */
+    public getVisitor (transformationStage: TransformationStage): IVisitor | null {
+        switch (transformationStage) {
+            case TransformationStage.Converting:
+                return {
+                    leave: (node: ESTree.Node, parentNode: ESTree.Node | null) => {
+                        if (!this.options.splitStrings) {
+                            return;
+                        }
+
+                        if (parentNode && NodeGuards.isLiteralNode(node)) {
+                            return this.transformNode(node, parentNode);
+                        }
+                    }
+                };
+
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * @param {Literal} literalNode
+     * @param {Node} parentNode
+     * @returns {Node}
+     */
+    public transformNode (literalNode: ESTree.Literal, parentNode: ESTree.Node): ESTree.Node {
+        if (typeof literalNode.value !== 'string') {
+            return literalNode;
+        }
+
+        if (NodeGuards.isPropertyNode(parentNode) && !parentNode.computed) {
+            return literalNode;
+        }
+
+        if (this.options.splitStringsChunkLength >= literalNode.value.length) {
+            return literalNode;
+        }
+
+        const stringChunks: string[] = SplitStringTransformer.chunkString(
+            literalNode.value,
+            this.options.splitStringsChunkLength
+        );
+
+        return this.transformStringChunksToBinaryExpressionNode(stringChunks);
+    }
+
+    /**
+     * @param {string[]} chunks
+     * @returns {BinaryExpression}
+     */
+    private transformStringChunksToBinaryExpressionNode (chunks: string[]): ESTree.BinaryExpression | ESTree.Literal {
+        const lastChunk: string | undefined = chunks.pop();
+
+        if (lastChunk === undefined) {
+            throw new Error('Last chunk value should not be empty');
+        }
+
+        const lastChunkLiteralNode: ESTree.Literal = NodeFactory.literalNode(lastChunk);
+
+        if (chunks.length === 0) {
+            return lastChunkLiteralNode;
+        }
+
+        return NodeFactory.binaryExpressionNode(
+            '+',
+            this.transformStringChunksToBinaryExpressionNode(chunks),
+            lastChunkLiteralNode
+        );
+    }
+}

+ 14 - 0
src/options/Options.ts

@@ -204,6 +204,20 @@ export class Options implements IOptions {
     @IsIn([SourceMapMode.Inline, SourceMapMode.Separate])
     public readonly sourceMapMode!: SourceMapMode;
 
+    /**
+     * @type {boolean}
+     */
+    @IsBoolean()
+    public readonly splitStrings!: boolean;
+
+    /**
+     * @type {number}
+     */
+    @IsNumber()
+    @ValidateIf((options: IOptions) => Boolean(options.splitStrings))
+    @Min(1)
+    public readonly splitStringsChunkLength!: number;
+
     /**
      * @type {boolean}
      */

+ 2 - 0
src/options/OptionsNormalizer.ts

@@ -13,6 +13,7 @@ import { InputFileNameRule } from './normalizer-rules/InputFileNameRule';
 import { SelfDefendingRule } from './normalizer-rules/SelfDefendingRule';
 import { SourceMapBaseUrlRule } from './normalizer-rules/SourceMapBaseUrlRule';
 import { SourceMapFileNameRule } from './normalizer-rules/SourceMapFileNameRule';
+import { SplitStringsChunkLengthRule } from './normalizer-rules/SplitStringsChunkLengthRule';
 import { StringArrayRule } from './normalizer-rules/StringArrayRule';
 import { StringArrayEncodingRule } from './normalizer-rules/StringArrayEncodingRule';
 import { StringArrayThresholdRule } from './normalizer-rules/StringArrayThresholdRule';
@@ -31,6 +32,7 @@ export class OptionsNormalizer implements IOptionsNormalizer {
         SelfDefendingRule,
         SourceMapBaseUrlRule,
         SourceMapFileNameRule,
+        SplitStringsChunkLengthRule,
         StringArrayRule,
         StringArrayEncodingRule,
         StringArrayThresholdRule,

+ 24 - 0
src/options/normalizer-rules/SplitStringsChunkLengthRule.ts

@@ -0,0 +1,24 @@
+import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRule';
+
+import { IOptions } from '../../interfaces/options/IOptions';
+
+/**
+ * @param {IOptions} options
+ * @returns {IOptions}
+ */
+export const SplitStringsChunkLengthRule: TOptionsNormalizerRule = (options: IOptions): IOptions => {
+    if (options.splitStringsChunkLength === 0) {
+        options = {
+            ...options,
+            splitStrings: false,
+            splitStringsChunkLength: 0
+        };
+    } else {
+        options = {
+            ...options,
+            splitStringsChunkLength: Math.floor(options.splitStringsChunkLength)
+        };
+    }
+
+    return options;
+};

+ 2 - 0
src/options/presets/Default.ts

@@ -30,6 +30,8 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapMode: SourceMapMode.Separate,
+    splitStrings: false,
+    splitStringsChunkLength: 10,
     stringArray: true,
     stringArrayEncoding: false,
     stringArrayThreshold: 0.75,

+ 2 - 0
src/options/presets/NoCustomNodes.ts

@@ -29,6 +29,8 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapMode: SourceMapMode.Separate,
+    splitStrings: false,
+    splitStringsChunkLength: 0,
     stringArray: false,
     stringArrayEncoding: false,
     stringArrayThreshold: 0,

+ 5 - 3
test/dev/dev.ts

@@ -6,13 +6,15 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-        var n;
-        (n = {foo: 'bar'}).baz = n.foo;
+        var n = 'abcefgi';
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
             compact: false,
-            transformObjectKeys: true,
+            splitStrings: true,
+            splitStringsChunkLength: 4,
+            stringArray: true,
+            stringArrayThreshold: 1,
             seed: 1
         }
     ).getObfuscatedCode();

+ 130 - 0
test/functional-tests/node-transformers/converting-transformers/split-string-transformer/SplitStringTransformer.spec.ts

@@ -0,0 +1,130 @@
+import { assert } from 'chai';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
+
+import { readFileAsString } from '../../../../helpers/readFileAsString';
+
+import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
+
+describe('SplitStringTransformer', () => {
+    let obfuscatedCode: string;
+    
+    describe('Variant #1: simple string literal', () => {
+        it('should transform string literal to binary expression', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 2
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *'ab' *\+ *'cd' *\+ *'ef' *\+ *'g';$/);
+        });
+    });
+
+    describe('Variant #2: `splitStrings` option is disabled', () => {
+        it('should keep original string literal', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: false,
+                    splitStringsChunkLength: 10
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *'abcdefg';$/);
+        });
+    });
+
+    describe('Variant #3: `splitStringsChunkLength` value larger than string size', () => {
+        it('should keep original string literal', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 10
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *'abcdefg';$/);
+        });
+    });
+
+    describe('Variant #4: `splitStringsChunkLength` value is `0`', () => {
+        it('should throw an validation error ', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+
+            const testFunc = () => JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 0
+                }
+            );
+
+            assert.throws(testFunc, /validation failed/i);
+        });
+    });
+
+    describe('Variant #5: strings concatenation', () => {
+        it('should transform string literals to binary expressions', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/strings-concatenation.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 2
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *'ab' *\+ *'cd' *\+ *\( *'ef' *\+ *'g' *\);$/);
+        });
+    });
+
+    describe('Variant #6: object key string literal', () => {
+        it('should keep original string literal', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/object-key-string-literal.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 2
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *{'abcdefg' *: *0x1};$/);
+        });
+    });
+
+    describe('Variant #7: object computed key string literal', () => {
+        it('should transform string literal to binary expression', () => {
+            const code: string = readFileAsString(__dirname + '/fixtures/object-computed-key-string-literal.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    splitStrings: true,
+                    splitStringsChunkLength: 2
+                }
+            ).getObfuscatedCode();
+
+            assert.match(obfuscatedCode,  /^var *test *= *{\['ab' *\+ *'cd' *\+ *'ef' *\+ *'g'] *: *0x1};$/);
+        });
+    });
+});

+ 3 - 0
test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/object-computed-key-string-literal.js

@@ -0,0 +1,3 @@
+var test = {
+    ['abcdefg']: 1
+};

+ 3 - 0
test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/object-key-string-literal.js

@@ -0,0 +1,3 @@
+var test = {
+    'abcdefg': 1
+};

+ 1 - 0
test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/simple-input.js

@@ -0,0 +1 @@
+var test = 'abcdefg';

+ 1 - 0
test/functional-tests/node-transformers/converting-transformers/split-string-transformer/fixtures/strings-concatenation.js

@@ -0,0 +1 @@
+var test = 'abcd' + 'efg';

+ 22 - 0
test/functional-tests/options/OptionsNormalizer.spec.ts

@@ -330,6 +330,28 @@ describe('OptionsNormalizer', () => {
             });
         });
 
+        describe('splitStringsChunkLengthRule', () => {
+            describe('`splitStringsChunkLengthRule` value is float number', () => {
+                before(() => {
+                    optionsPreset = getNormalizedOptions({
+                        ...DEFAULT_PRESET,
+                        splitStrings: true,
+                        splitStringsChunkLength: 5.6
+                    });
+
+                    expectedOptionsPreset = {
+                        ...DEFAULT_PRESET,
+                        splitStrings: true,
+                        splitStringsChunkLength: 5
+                    };
+                });
+
+                it('should normalize options preset', () => {
+                    assert.deepEqual(optionsPreset, expectedOptionsPreset);
+                });
+            });
+        });
+
         describe('stringArrayRule', () => {
             before(() => {
                 optionsPreset = getNormalizedOptions({

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.