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