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