/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.archives; import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkNotNull; import static com.android.documentsui.archives.ArchiveRegistry.COMMON_ARCHIVE_TYPE; import static com.android.documentsui.archives.ArchiveRegistry.SEVEN_Z_TYPE; import static com.android.documentsui.archives.ArchiveRegistry.ZIP_TYPE; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import java.io.Closeable; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.channels.SeekableByteChannel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.sevenz.SevenZFile; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; /** * To handle to all of supported support types of archive or compressed+archive files. * @param the archive class such as SevenZFile, ZipFile, ArchiveInputStream etc. */ abstract class ArchiveHandle implements Closeable { private static final String TAG = ArchiveHandle.class.getSimpleName(); /** * To re-create the CommonArchive that belongs to SevenZFile, ZipFile, or * ArchiveInputStream. It needs file descriptor to create the input stream or seek to the head. */ @NonNull private final ParcelFileDescriptor mParcelFileDescriptor; /** * To re-create the CommonArchive that belongs to SevenZFile, ZipFile, or * ArchiveInputStream. It needs MIME type to know how to re-create. */ @NonNull private final String mMimeType; /** * CommonArchive is generic type. It may be SevenZFile, ZipFile, or ArchiveInputStream. */ @NonNull private T mCommonArchive; /** * To use factory pattern ensure the only one way to create the ArchiveHandle instance. * @param parcelFileDescriptor the file descriptor * @param mimeType the mime type of the file * @param commonArchive the common archive instance */ private ArchiveHandle(@NonNull ParcelFileDescriptor parcelFileDescriptor, @NonNull String mimeType, @NonNull T commonArchive) { mParcelFileDescriptor = parcelFileDescriptor; mMimeType = mimeType; mCommonArchive = commonArchive; } /** * It's used to re-create the file input stream. Just like SevenZFile or ArchiveInputStream. * * @return the file input stream */ @NonNull private FileInputStream recreateCommonArchiveStream() throws IOException { FileInputStream fileInputStream = new FileInputStream(mParcelFileDescriptor.getFileDescriptor()); SeekableByteChannel seekableByteChannel = fileInputStream.getChannel(); seekableByteChannel.position(0); return fileInputStream; } /** * To get the MIME type of the file. * @return the MIME type of file */ @NonNull protected String getMimeType() { return mMimeType; } /** * To get the common archive instance. * * @return the common archive instance. */ @NonNull public final T getCommonArchive() { return mCommonArchive; } private void setCommonArchive(@NonNull T commonArchive) { mCommonArchive = commonArchive; } /** * Neither SevenZFile nor ArchiveInputStream likes ZipFile that has the API * getInputStream(ArchiveEntry), rewind or reset, so it needs to close the * current instance and recreate a new one. * * @param archiveEntry the target entry * @return the input stream related to archiveEntry * @throws IOException invalid file descriptor may raise the IOException * @throws CompressorException invalid compress name may raise the CompressException * @throws ArchiveException invalid Archive name may raise the ArchiveException */ protected InputStream getInputStream(@NonNull ArchiveEntry archiveEntry) throws IOException, CompressorException, ArchiveException { if (!isCommonArchiveSupportGetInputStream()) { FileInputStream fileInputStream = recreateCommonArchiveStream(); T commonArchive = recreateCommonArchive(fileInputStream); if (commonArchive != null) { closeCommonArchive(); setCommonArchive(commonArchive); } else { Log.e(TAG, "new SevenZFile or ArchiveInputStream is null"); fileInputStream.close(); } } return ArchiveEntryInputStream.create(this, archiveEntry); } boolean isCommonArchiveSupportGetInputStream() { return false; } void closeCommonArchive() throws IOException { throw new UnsupportedOperationException("This kind of ArchiveHandle doesn't support"); } T recreateCommonArchive(FileInputStream fileInputStream) throws CompressorException, ArchiveException, IOException { throw new UnsupportedOperationException("This kind of ArchiveHandle doesn't support"); } public void close() throws IOException { mParcelFileDescriptor.close(); } /** * To get the enumeration of all of entries from archive. * @return the enumeration of all of entries from archive * @throws IOException it may raise the IOException when the archiveHandle get the next entry */ @NonNull public abstract Enumeration getEntries() throws IOException; private static class SevenZFileHandle extends ArchiveHandle { SevenZFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, SevenZFile commonArchive) { super(parcelFileDescriptor, mimeType, commonArchive); } @Override protected void closeCommonArchive() throws IOException { getCommonArchive().close(); } @Override protected SevenZFile recreateCommonArchive(@NonNull FileInputStream fileInputStream) throws IOException { return new SevenZFile(fileInputStream.getChannel()); } @NonNull @Override public Enumeration getEntries() { if (getCommonArchive().getEntries() == null) { return Collections.emptyEnumeration(); } return Collections.enumeration( (Collection) getCommonArchive().getEntries()); } } private static class ZipFileHandle extends ArchiveHandle { ZipFileHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, ZipFile commonArchive) { super(parcelFileDescriptor, mimeType, commonArchive); } @Override protected boolean isCommonArchiveSupportGetInputStream() { return true; } @NonNull @Override public Enumeration getEntries() { final Enumeration enumeration = getCommonArchive().getEntries(); if (enumeration == null) { return Collections.emptyEnumeration(); } return enumeration; } } private static class CommonArchiveInputHandle extends ArchiveHandle { CommonArchiveInputHandle(ParcelFileDescriptor parcelFileDescriptor, String mimeType, ArchiveInputStream commonArchive) { super(parcelFileDescriptor, mimeType, commonArchive); } @Override protected void closeCommonArchive() throws IOException { getCommonArchive().close(); } @Override protected ArchiveInputStream recreateCommonArchive(FileInputStream fileInputStream) throws CompressorException, ArchiveException { return createCommonArchive(fileInputStream, getMimeType()); } @NonNull @Override public Enumeration getEntries() throws IOException { final ArchiveInputStream archiveInputStream = getCommonArchive(); final List list = new ArrayList<>(); ArchiveEntry entry; while ((entry = archiveInputStream.getNextEntry()) != null) { list.add(entry); } return Collections.enumeration(list); } } @NonNull private static ArchiveInputStream createCommonArchive( @NonNull FileInputStream fileInputStream, @NonNull String mimeType) throws CompressorException, ArchiveException { InputStream inputStream = fileInputStream; String compressName = ArchiveRegistry.getCompressName(mimeType); if (!TextUtils.isEmpty(compressName)) { CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); inputStream = compressorStreamFactory .createCompressorInputStream(compressName, inputStream); } ArchiveStreamFactory archiveStreamFactory = new ArchiveStreamFactory(); String archiveName = ArchiveRegistry.getArchiveName(mimeType); if (TextUtils.isEmpty(archiveName)) { throw new ArchiveException("Invalid archive name."); } return archiveStreamFactory .createArchiveInputStream(archiveName, inputStream); } /** * The only one way creates the instance of ArchiveHandle. */ public static ArchiveHandle create(@NonNull ParcelFileDescriptor parcelFileDescriptor, @NonNull String mimeType) throws CompressorException, ArchiveException, IOException { checkNotNull(parcelFileDescriptor); checkArgument(!TextUtils.isEmpty(mimeType)); Integer archiveType = ArchiveRegistry.getArchiveType(mimeType); if (archiveType == null) { throw new UnsupportedOperationException("Doesn't support MIME type " + mimeType); } FileInputStream fileInputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor()); switch (archiveType) { case COMMON_ARCHIVE_TYPE: ArchiveInputStream archiveInputStream = createCommonArchive(fileInputStream, mimeType); return new CommonArchiveInputHandle(parcelFileDescriptor, mimeType, archiveInputStream); case ZIP_TYPE: SeekableByteChannel zipFileChannel = fileInputStream.getChannel(); try { ZipFile zipFile = new ZipFile(zipFileChannel); return new ZipFileHandle(parcelFileDescriptor, mimeType, zipFile); } catch (Exception e) { FileUtils.closeQuietly(zipFileChannel); throw e; } case SEVEN_Z_TYPE: SeekableByteChannel sevenZFileChannel = fileInputStream.getChannel(); try { SevenZFile sevenZFile = new SevenZFile(sevenZFileChannel); return new SevenZFileHandle(parcelFileDescriptor, mimeType, sevenZFile); } catch (Exception e) { FileUtils.closeQuietly(sevenZFileChannel); throw e; } default: throw new UnsupportedOperationException("Doesn't support MIME type " + mimeType); } } }