1 /* 2 * Copyright (C) 2019 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 */ 16 17 package com.android.documentsui.archives; 18 19 import static androidx.core.util.Preconditions.checkArgument; 20 import static androidx.core.util.Preconditions.checkNotNull; 21 22 import static com.android.documentsui.archives.ArchiveRegistry.COMMON_ARCHIVE_TYPE; 23 import static com.android.documentsui.archives.ArchiveRegistry.SEVEN_Z_TYPE; 24 import static com.android.documentsui.archives.ArchiveRegistry.ZIP_TYPE; 25 26 import android.os.FileUtils; 27 import android.os.ParcelFileDescriptor; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 33 import java.io.Closeable; 34 import java.io.FileInputStream; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.nio.channels.SeekableByteChannel; 38 import java.util.ArrayList; 39 import java.util.Collection; 40 import java.util.Collections; 41 import java.util.Enumeration; 42 import java.util.List; 43 44 import org.apache.commons.compress.archivers.ArchiveEntry; 45 import org.apache.commons.compress.archivers.ArchiveException; 46 import org.apache.commons.compress.archivers.ArchiveInputStream; 47 import org.apache.commons.compress.archivers.ArchiveStreamFactory; 48 import org.apache.commons.compress.archivers.sevenz.SevenZFile; 49 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 50 import org.apache.commons.compress.archivers.zip.ZipFile; 51 import org.apache.commons.compress.compressors.CompressorException; 52 import org.apache.commons.compress.compressors.CompressorStreamFactory; 53 54 /** 55 * To handle to all of supported support types of archive or compressed+archive files. 56 * @param <T> the archive class such as SevenZFile, ZipFile, ArchiveInputStream etc. 57 */ 58 abstract class ArchiveHandle<T> implements Closeable { 59 private static final String TAG = ArchiveHandle.class.getSimpleName(); 60 /** 61 * To re-create the CommonArchive that belongs to SevenZFile, ZipFile, or 62 * ArchiveInputStream. It needs file descriptor to create the input stream or seek to the head. 63 */ 64 @NonNull 65 private final ParcelFileDescriptor mParcelFileDescriptor; 66 67 /** 68 * To re-create the CommonArchive that belongs to SevenZFile, ZipFile, or 69 * ArchiveInputStream. It needs MIME type to know how to re-create. 70 */ 71 @NonNull 72 private final String mMimeType; 73 74 /** 75 * CommonArchive is generic type. It may be SevenZFile, ZipFile, or ArchiveInputStream. 76 */ 77 @NonNull 78 private T mCommonArchive; 79 80 /** 81 * To use factory pattern ensure the only one way to create the ArchiveHandle instance. 82 * @param parcelFileDescriptor the file descriptor 83 * @param mimeType the mime type of the file 84 * @param commonArchive the common archive instance 85 */ ArchiveHandle(@onNull ParcelFileDescriptor parcelFileDescriptor, @NonNull String mimeType, @NonNull T commonArchive)86 private ArchiveHandle(@NonNull ParcelFileDescriptor parcelFileDescriptor, 87 @NonNull String mimeType, 88 @NonNull T commonArchive) { 89 mParcelFileDescriptor = parcelFileDescriptor; 90 mMimeType = mimeType; 91 mCommonArchive = commonArchive; 92 } 93 94 /** 95 * It's used to re-create the file input stream. Just like SevenZFile or ArchiveInputStream. 96 * 97 * @return the file input stream 98 */ 99 @NonNull recreateCommonArchiveStream()100 private FileInputStream recreateCommonArchiveStream() throws IOException { 101 FileInputStream fileInputStream = 102 new FileInputStream(mParcelFileDescriptor.getFileDescriptor()); 103 SeekableByteChannel seekableByteChannel = fileInputStream.getChannel(); 104 seekableByteChannel.position(0); 105 return fileInputStream; 106 } 107 108 /** 109 * To get the MIME type of the file. 110 * @return the MIME type of file 111 */ 112 @NonNull getMimeType()113 protected String getMimeType() { 114 return mMimeType; 115 } 116 117 /** 118 * To get the common archive instance. 119 * 120 * @return the common archive instance. 121 */ 122 @NonNull getCommonArchive()123 public final T getCommonArchive() { 124 return mCommonArchive; 125 } 126 setCommonArchive(@onNull T commonArchive)127 private void setCommonArchive(@NonNull T commonArchive) { 128 mCommonArchive = commonArchive; 129 } 130 131 /** 132 * Neither SevenZFile nor ArchiveInputStream likes ZipFile that has the API 133 * getInputStream(ArchiveEntry), rewind or reset, so it needs to close the 134 * current instance and recreate a new one. 135 * 136 * @param archiveEntry the target entry 137 * @return the input stream related to archiveEntry 138 * @throws IOException invalid file descriptor may raise the IOException 139 * @throws CompressorException invalid compress name may raise the CompressException 140 * @throws ArchiveException invalid Archive name may raise the ArchiveException 141 */ getInputStream(@onNull ArchiveEntry archiveEntry)142 protected InputStream getInputStream(@NonNull ArchiveEntry archiveEntry) 143 throws IOException, CompressorException, ArchiveException { 144 145 if (!isCommonArchiveSupportGetInputStream()) { 146 FileInputStream fileInputStream = recreateCommonArchiveStream(); 147 T commonArchive = recreateCommonArchive(fileInputStream); 148 if (commonArchive != null) { 149 closeCommonArchive(); 150 setCommonArchive(commonArchive); 151 } else { 152 Log.e(TAG, "new SevenZFile or ArchiveInputStream is null"); 153 fileInputStream.close(); 154 } 155 } 156 157 return ArchiveEntryInputStream.create(this, archiveEntry); 158 } 159 isCommonArchiveSupportGetInputStream()160 boolean isCommonArchiveSupportGetInputStream() { 161 return false; 162 } 163 closeCommonArchive()164 void closeCommonArchive() throws IOException { 165 throw new UnsupportedOperationException("This kind of ArchiveHandle doesn't support"); 166 } 167 recreateCommonArchive(FileInputStream fileInputStream)168 T recreateCommonArchive(FileInputStream fileInputStream) 169 throws CompressorException, ArchiveException, IOException { 170 throw new UnsupportedOperationException("This kind of ArchiveHandle doesn't support"); 171 } 172 close()173 public void close() throws IOException { 174 mParcelFileDescriptor.close(); 175 } 176 177 /** 178 * To get the enumeration of all of entries from archive. 179 * @return the enumeration of all of entries from archive 180 * @throws IOException it may raise the IOException when the archiveHandle get the next entry 181 */ 182 @NonNull getEntries()183 public abstract Enumeration<? extends ArchiveEntry> getEntries() throws IOException; 184 185 private static class SevenZFileHandle extends ArchiveHandle<SevenZFile> { SevenZFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, SevenZFile commonArchive)186 SevenZFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, 187 SevenZFile commonArchive) { 188 super(parcelFileDescriptor, mimeType, commonArchive); 189 } 190 191 @Override closeCommonArchive()192 protected void closeCommonArchive() throws IOException { 193 getCommonArchive().close(); 194 } 195 196 @Override recreateCommonArchive(@onNull FileInputStream fileInputStream)197 protected SevenZFile recreateCommonArchive(@NonNull FileInputStream fileInputStream) 198 throws IOException { 199 return new SevenZFile(fileInputStream.getChannel()); 200 } 201 202 @NonNull 203 @Override getEntries()204 public Enumeration<? extends ArchiveEntry> getEntries() { 205 if (getCommonArchive().getEntries() == null) { 206 return Collections.emptyEnumeration(); 207 } 208 209 return Collections.enumeration( 210 (Collection<? extends ArchiveEntry>) getCommonArchive().getEntries()); 211 } 212 } 213 214 private static class ZipFileHandle extends ArchiveHandle<ZipFile> { ZipFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, ZipFile commonArchive)215 ZipFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, 216 ZipFile commonArchive) { 217 super(parcelFileDescriptor, mimeType, commonArchive); 218 } 219 220 @Override isCommonArchiveSupportGetInputStream()221 protected boolean isCommonArchiveSupportGetInputStream() { 222 return true; 223 } 224 225 @NonNull 226 @Override getEntries()227 public Enumeration<? extends ArchiveEntry> getEntries() { 228 final Enumeration<ZipArchiveEntry> enumeration = getCommonArchive().getEntries(); 229 if (enumeration == null) { 230 return Collections.emptyEnumeration(); 231 } 232 return enumeration; 233 } 234 } 235 236 private static class CommonArchiveInputHandle extends ArchiveHandle<ArchiveInputStream> { CommonArchiveInputHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, ArchiveInputStream commonArchive)237 CommonArchiveInputHandle(ParcelFileDescriptor parcelFileDescriptor, 238 String mimeType, ArchiveInputStream commonArchive) { 239 super(parcelFileDescriptor, mimeType, commonArchive); 240 } 241 242 @Override closeCommonArchive()243 protected void closeCommonArchive() throws IOException { 244 getCommonArchive().close(); 245 } 246 247 @Override recreateCommonArchive(FileInputStream fileInputStream)248 protected ArchiveInputStream recreateCommonArchive(FileInputStream fileInputStream) 249 throws CompressorException, ArchiveException { 250 return createCommonArchive(fileInputStream, getMimeType()); 251 } 252 253 @NonNull 254 @Override getEntries()255 public Enumeration<? extends ArchiveEntry> getEntries() throws IOException { 256 final ArchiveInputStream archiveInputStream = getCommonArchive(); 257 final List<ArchiveEntry> list = new ArrayList<>(); 258 ArchiveEntry entry; 259 while ((entry = archiveInputStream.getNextEntry()) != null) { 260 list.add(entry); 261 } 262 263 return Collections.enumeration(list); 264 } 265 } 266 267 @NonNull createCommonArchive( @onNull FileInputStream fileInputStream, @NonNull String mimeType)268 private static ArchiveInputStream createCommonArchive( 269 @NonNull FileInputStream fileInputStream, 270 @NonNull String mimeType) throws CompressorException, ArchiveException { 271 InputStream inputStream = fileInputStream; 272 273 String compressName = ArchiveRegistry.getCompressName(mimeType); 274 if (!TextUtils.isEmpty(compressName)) { 275 CompressorStreamFactory compressorStreamFactory = 276 new CompressorStreamFactory(); 277 inputStream = compressorStreamFactory 278 .createCompressorInputStream(compressName, inputStream); 279 } 280 281 ArchiveStreamFactory archiveStreamFactory = new ArchiveStreamFactory(); 282 String archiveName = ArchiveRegistry.getArchiveName(mimeType); 283 if (TextUtils.isEmpty(archiveName)) { 284 throw new ArchiveException("Invalid archive name."); 285 } 286 287 return archiveStreamFactory 288 .createArchiveInputStream(archiveName, inputStream); 289 } 290 291 /** 292 * The only one way creates the instance of ArchiveHandle. 293 */ create(@onNull ParcelFileDescriptor parcelFileDescriptor, @NonNull String mimeType)294 public static ArchiveHandle create(@NonNull ParcelFileDescriptor parcelFileDescriptor, 295 @NonNull String mimeType) throws CompressorException, ArchiveException, IOException { 296 checkNotNull(parcelFileDescriptor); 297 checkArgument(!TextUtils.isEmpty(mimeType)); 298 299 Integer archiveType = ArchiveRegistry.getArchiveType(mimeType); 300 if (archiveType == null) { 301 throw new UnsupportedOperationException("Doesn't support MIME type " + mimeType); 302 } 303 304 FileInputStream fileInputStream = 305 new FileInputStream(parcelFileDescriptor.getFileDescriptor()); 306 307 switch (archiveType) { 308 case COMMON_ARCHIVE_TYPE: 309 ArchiveInputStream archiveInputStream = 310 createCommonArchive(fileInputStream, mimeType); 311 return new CommonArchiveInputHandle(parcelFileDescriptor, mimeType, 312 archiveInputStream); 313 case ZIP_TYPE: 314 SeekableByteChannel zipFileChannel = fileInputStream.getChannel(); 315 try { 316 ZipFile zipFile = new ZipFile(zipFileChannel); 317 return new ZipFileHandle(parcelFileDescriptor, mimeType, 318 zipFile); 319 } catch (Exception e) { 320 FileUtils.closeQuietly(zipFileChannel); 321 throw e; 322 } 323 case SEVEN_Z_TYPE: 324 SeekableByteChannel sevenZFileChannel = fileInputStream.getChannel(); 325 try { 326 SevenZFile sevenZFile = new SevenZFile(sevenZFileChannel); 327 return new SevenZFileHandle(parcelFileDescriptor, mimeType, 328 sevenZFile); 329 } catch (Exception e) { 330 FileUtils.closeQuietly(sevenZFileChannel); 331 throw e; 332 } 333 default: 334 throw new UnsupportedOperationException("Doesn't support MIME type " 335 + mimeType); 336 } 337 } 338 } 339