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(files: File[]): Promise<Blob> {
54    const zip = new JSZip();
55    for (let i = 0; i < files.length; i++) {
56      const file = files[i];
57      const blob = await file.arrayBuffer();
58      zip.file(file.name, blob);
59    }
60    return await zip.generateAsync({type: 'blob'});
61  }
62
63  static async unzipFile(
64    file: Blob,
65    onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING,
66  ): Promise<File[]> {
67    const unzippedFiles: File[] = [];
68    const zip = new JSZip();
69    const content = await zip.loadAsync(file);
70
71    const filenames = Object.keys(content.files);
72    for (const [index, filename] of filenames.entries()) {
73      const file = content.files[filename];
74      if (file.dir) {
75        // Ignore directories
76        continue;
77      } else {
78        const fileBlob = await file.async('blob');
79        const unzippedFile = new File([fileBlob], filename);
80        unzippedFiles.push(unzippedFile);
81      }
82
83      onProgressUpdate((100 * (index + 1)) / filenames.length);
84    }
85
86    return unzippedFiles;
87  }
88
89  static async decompressGZipFile(file: File): Promise<File> {
90    const decompressionStream = new (window as any).DecompressionStream('gzip');
91    const decompressedStream = file.stream().pipeThrough(decompressionStream);
92    const fileBlob = await new Response(decompressedStream).blob();
93    return new File(
94      [fileBlob],
95      FileUtils.removeExtensionFromFilename(file.name),
96    );
97  }
98
99  static async isZipFile(file: File): Promise<boolean> {
100    return FileUtils.isMatchForMagicNumber(file, FileUtils.PK_ZIP_MAGIC_NUMBER);
101  }
102
103  static async isGZipFile(file: File): Promise<boolean> {
104    return FileUtils.isMatchForMagicNumber(file, FileUtils.GZIP_MAGIC_NUMBER);
105  }
106
107  private static async isMatchForMagicNumber(
108    file: File,
109    magicNumber: number[],
110  ): Promise<boolean> {
111    const bufferStart = new Uint8Array((await file.arrayBuffer()).slice(0, 2));
112    return ArrayUtils.equal(bufferStart, magicNumber);
113  }
114
115  private static readonly GZIP_MAGIC_NUMBER = [0x1f, 0x8b];
116  private static readonly PK_ZIP_MAGIC_NUMBER = [0x50, 0x4b];
117}
118