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