1 /* 2 * Copyright 2022 Google LLC 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 */ 16 package com.google.android.libraries.mobiledatadownload.internal.downloader; 17 18 import android.net.Uri; 19 import com.google.android.libraries.mobiledatadownload.file.OpenContext; 20 import com.google.android.libraries.mobiledatadownload.file.Opener; 21 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 22 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; 23 import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener; 24 import com.google.common.io.ByteStreams; 25 import java.io.IOException; 26 import java.io.OutputStream; 27 import java.util.zip.ZipEntry; 28 import java.util.zip.ZipException; 29 import java.util.zip.ZipInputStream; 30 31 /** 32 * An opener takes in an output folder URI and expands all resources in the zip input stream to the 33 * folder. 34 */ 35 public final class ZipFolderOpener implements Opener<Void> { 36 37 private final Uri targetFolderUri; 38 private final SaferZipUtils zipUtils; 39 ZipFolderOpener(Uri targetFolderUri)40 private ZipFolderOpener(Uri targetFolderUri) { 41 this.targetFolderUri = targetFolderUri; 42 this.zipUtils = new SaferZipUtils() {}; 43 } 44 create(Uri targetFolderUri)45 public static ZipFolderOpener create(Uri targetFolderUri) { 46 return new ZipFolderOpener(targetFolderUri); 47 } 48 49 @Override open(OpenContext openContext)50 public Void open(OpenContext openContext) throws IOException { 51 SynchronousFileStorage fileStorage = openContext.storage(); 52 try (ZipInputStream zipInputStream = 53 new ZipInputStream(ReadStreamOpener.create().withBufferedIo().open(openContext))) { 54 // Iterate all entries and write to target URI one by one 55 ZipEntry zipEntry; 56 while ((zipEntry = zipInputStream.getNextEntry()) != null) { 57 String path = zipUtils.getValidatedName(zipEntry); 58 Uri uri = targetFolderUri.buildUpon().appendPath(path).build(); 59 if (zipEntry.isDirectory()) { 60 fileStorage.createDirectory(uri); 61 } else { 62 try (OutputStream out = fileStorage.open(uri, WriteStreamOpener.create())) { 63 ByteStreams.copy(zipInputStream, out); 64 } 65 } 66 } 67 } catch (IOException ioe) { 68 // Cleanup the target directory if any error occurred. 69 fileStorage.deleteRecursively(targetFolderUri); 70 throw ioe; 71 } 72 return null; 73 } 74 75 /** Utilities for safely accessing ZipEntry APIs. */ 76 private interface SaferZipUtils { 77 /** 78 * Return the name of a ZipEntry after verifying that it does not exploit any path traversal 79 * attacks. 80 * 81 * @throws ZipException if {@code zipEntry} contains any possible path traversal characters. 82 */ getValidatedName(ZipEntry entry)83 default String getValidatedName(ZipEntry entry) throws ZipException { 84 return entry.getName(); 85 } 86 } 87 } 88