xref: /aosp_15_r20/development/tools/winscope/src/common/file_utils.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import JSZip from 'jszip';
17import {ArrayUtils} from './array_utils';
18import {FunctionUtils, OnProgressUpdateType} from './function_utils';
19
20export type OnFile = (file: File, parentArchive: File | undefined) => void;
21
22export class FileUtils {
23  //allow: letters/numbers/underscores with delimiters . - # (except at start and end)
24  static readonly DOWNLOAD_FILENAME_REGEX = /^\w+?((|#|-|\.)\w+)+$/;
25  static readonly ILLEGAL_FILENAME_CHARACTERS_REGEX = /[^A-Za-z0-9-#._]/g;
26
27  static getFileExtension(filename: string): string | undefined {
28    const lastDot = filename.lastIndexOf('.');
29    if (lastDot === -1) {
30      return undefined;
31    }
32    return filename.slice(lastDot + 1);
33  }
34
35  static removeDirFromFileName(name: string): string {
36    if (name.includes('/')) {
37      const startIndex = name.lastIndexOf('/') + 1;
38      return name.slice(startIndex);
39    } else {
40      return name;
41    }
42  }
43
44  static removeExtensionFromFilename(name: string): string {
45    if (name.includes('.')) {
46      const lastIndex = name.lastIndexOf('.');
47      return name.slice(0, lastIndex);
48    } else {
49      return name;
50    }
51  }
52
53  static async createZipArchive(
54    files: File[],
55    progressCallback?: OnProgressUpdateType,
56  ): Promise<Blob> {
57    const zip = new JSZip();
58    for (let i = 0; i < files.length; i++) {
59      const file = files[i];
60      const blob = await file.arrayBuffer();
61      zip.file(file.name, blob);
62      if (progressCallback) progressCallback((i + 1) / files.length);
63    }
64    return await zip.generateAsync({type: 'blob'});
65  }
66
67  static async unzipFile(
68    file: Blob,
69    onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING,
70  ): Promise<File[]> {
71    const unzippedFiles: File[] = [];
72    const zip = new JSZip();
73    const content = await zip.loadAsync(file);
74
75    const filenames = Object.keys(content.files);
76    for (const [index, filename] of filenames.entries()) {
77      const file = content.files[filename];
78      if (file.dir) {
79        // Ignore directories
80        continue;
81      } else {
82        const fileBlob = await file.async('blob');
83        const unzippedFile = new File([fileBlob], filename);
84        if (await FileUtils.isZipFile(unzippedFile)) {
85          unzippedFiles.push(...(await FileUtils.unzipFile(fileBlob)));
86        } else {
87          unzippedFiles.push(unzippedFile);
88        }
89      }
90
91      onProgressUpdate((100 * (index + 1)) / filenames.length);
92    }
93
94    return unzippedFiles;
95  }
96
97  static async decompressGZipFile(file: File): Promise<File> {
98    const decompressionStream = new (window as any).DecompressionStream('gzip');
99    const decompressedStream = file.stream().pipeThrough(decompressionStream);
100    const fileBlob = await new Response(decompressedStream).blob();
101    return new File(
102      [fileBlob],
103      FileUtils.removeExtensionFromFilename(file.name),
104    );
105  }
106
107  static async isZipFile(file: File): Promise<boolean> {
108    return FileUtils.isMatchForMagicNumber(file, FileUtils.PK_ZIP_MAGIC_NUMBER);
109  }
110
111  static async isGZipFile(file: File): Promise<boolean> {
112    return FileUtils.isMatchForMagicNumber(file, FileUtils.GZIP_MAGIC_NUMBER);
113  }
114
115  private static async isMatchForMagicNumber(
116    file: File,
117    magicNumber: number[],
118  ): Promise<boolean> {
119    const bufferStart = new Uint8Array((await file.arrayBuffer()).slice(0, 2));
120    return ArrayUtils.equal(bufferStart, magicNumber);
121  }
122
123  private static readonly GZIP_MAGIC_NUMBER = [0x1f, 0x8b];
124  private static readonly PK_ZIP_MAGIC_NUMBER = [0x50, 0x4b];
125}
126