1 /*
2  * Copyright (C) 2015 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 com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertTrue;
24 
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.ParcelFileDescriptor;
28 import android.provider.DocumentsContract.Document;
29 import android.system.ErrnoException;
30 import android.system.Os;
31 import android.system.OsConstants;
32 import android.text.TextUtils;
33 
34 import androidx.annotation.IdRes;
35 import androidx.test.InstrumentationRegistry;
36 import androidx.test.filters.MediumTest;
37 import androidx.test.runner.AndroidJUnit4;
38 
39 import com.android.documentsui.tests.R;
40 
41 import org.apache.commons.compress.archivers.ArchiveException;
42 import org.apache.commons.compress.compressors.CompressorException;
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 
48 import java.io.IOException;
49 import java.util.Scanner;
50 import java.util.concurrent.ExecutorService;
51 import java.util.concurrent.Executors;
52 import java.util.concurrent.TimeUnit;
53 
54 @RunWith(AndroidJUnit4.class)
55 @MediumTest
56 public class ReadableArchiveTest {
57 
58     private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
59     private static final String NOTIFICATION_URI =
60             "content://com.android.documentsui.archives/notification-uri";
61     private ExecutorService mExecutor = null;
62     private Archive mArchive = null;
63     private TestUtils mTestUtils = null;
64 
65     @Before
setUp()66     public void setUp() throws Exception {
67         mExecutor = Executors.newSingleThreadExecutor();
68         mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
69                 InstrumentationRegistry.getContext(), mExecutor);
70     }
71 
72     @After
tearDown()73     public void tearDown() throws Exception {
74         mExecutor.shutdown();
75         assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
76         if (mArchive != null) {
77             mArchive.close();
78         }
79     }
80 
createArchiveId(String path)81     public static ArchiveId createArchiveId(String path) {
82         return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_READ_ONLY, path);
83     }
84 
loadArchive(ParcelFileDescriptor descriptor, String mimeType)85     private void loadArchive(ParcelFileDescriptor descriptor, String mimeType)
86             throws IOException, CompressorException, ArchiveException {
87         mArchive = ReadableArchive.createForParcelFileDescriptor(
88                 InstrumentationRegistry.getTargetContext(),
89                 descriptor,
90                 ARCHIVE_URI,
91                 mimeType,
92                 ParcelFileDescriptor.MODE_READ_ONLY,
93                 Uri.parse(NOTIFICATION_URI));
94     }
95 
loadArchive(ParcelFileDescriptor descriptor)96     private void loadArchive(ParcelFileDescriptor descriptor)
97             throws IOException, CompressorException, ArchiveException {
98         loadArchive(descriptor, "application/zip");
99     }
100 
assertRowExist(Cursor cursor, String targetDocId)101     private static void assertRowExist(Cursor cursor, String targetDocId) {
102         assertTrue(cursor.moveToFirst());
103 
104         boolean found = false;
105         final int count = cursor.getCount();
106         for (int i = 0; i < count; i++) {
107             cursor.moveToPosition(i);
108             if (TextUtils.equals(targetDocId, cursor.getString(
109                     cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))) {
110                 found = true;
111                 break;
112             }
113         }
114 
115         assertTrue(targetDocId + " should be exists", found);
116     }
117 
118     @Test
testQueryChildDocument()119     public void testQueryChildDocument()
120             throws IOException, CompressorException, ArchiveException {
121         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
122         final Cursor cursor = mArchive.queryChildDocuments(
123                 createArchiveId("/").toDocumentId(), null, null);
124 
125         assertRowExist(cursor, createArchiveId("/file1.txt").toDocumentId());
126         assertEquals("file1.txt",
127                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
128         assertEquals("text/plain",
129                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
130         assertEquals(13,
131                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
132 
133         assertRowExist(cursor, createArchiveId("/dir1/").toDocumentId());
134         assertEquals("dir1",
135                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
136         assertEquals(Document.MIME_TYPE_DIR,
137                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
138         assertEquals(0,
139                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
140 
141         assertRowExist(cursor, createArchiveId("/dir2/").toDocumentId());
142         assertEquals("dir2",
143                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
144         assertEquals(Document.MIME_TYPE_DIR,
145                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
146         assertEquals(0,
147                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
148 
149         // Check if querying children works too.
150         final Cursor childCursor = mArchive.queryChildDocuments(
151                 createArchiveId("/dir1/").toDocumentId(), null, null);
152 
153         assertTrue(childCursor.moveToFirst());
154         assertEquals(
155                 createArchiveId("/dir1/cherries.txt").toDocumentId(),
156                 childCursor.getString(childCursor.getColumnIndexOrThrow(
157                         Document.COLUMN_DOCUMENT_ID)));
158         assertEquals("cherries.txt",
159                 childCursor.getString(childCursor.getColumnIndexOrThrow(
160                         Document.COLUMN_DISPLAY_NAME)));
161         assertEquals("text/plain",
162                 childCursor.getString(childCursor.getColumnIndexOrThrow(
163                         Document.COLUMN_MIME_TYPE)));
164         assertEquals(17,
165                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
166     }
167 
168     @Test
testQueryChildDocument_NoDirs()169     public void testQueryChildDocument_NoDirs()
170             throws IOException, CompressorException, ArchiveException {
171         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.no_dirs));
172         final Cursor cursor = mArchive.queryChildDocuments(
173                 createArchiveId("/").toDocumentId(), null, null);
174 
175         assertTrue(cursor.moveToFirst());
176         assertEquals(
177                 createArchiveId("/dir1/").toDocumentId(),
178                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
179         assertEquals("dir1",
180                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
181         assertEquals(Document.MIME_TYPE_DIR,
182                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
183         assertEquals(0,
184                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
185         assertFalse(cursor.moveToNext());
186 
187         final Cursor childCursor = mArchive.queryChildDocuments(
188                 createArchiveId("/dir1/").toDocumentId(), null, null);
189 
190         assertTrue(childCursor.moveToFirst());
191         assertEquals(
192                 createArchiveId("/dir1/dir2/").toDocumentId(),
193                 childCursor.getString(childCursor.getColumnIndexOrThrow(
194                         Document.COLUMN_DOCUMENT_ID)));
195         assertEquals("dir2",
196                 childCursor.getString(childCursor.getColumnIndexOrThrow(
197                         Document.COLUMN_DISPLAY_NAME)));
198         assertEquals(Document.MIME_TYPE_DIR,
199                 childCursor.getString(childCursor.getColumnIndexOrThrow(
200                         Document.COLUMN_MIME_TYPE)));
201         assertEquals(0,
202                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
203         assertFalse(childCursor.moveToNext());
204 
205         final Cursor childCursor2 = mArchive.queryChildDocuments(
206                 createArchiveId("/dir1/dir2/").toDocumentId(),
207                 null, null);
208 
209         assertTrue(childCursor2.moveToFirst());
210         assertEquals(
211                 createArchiveId("/dir1/dir2/cherries.txt").toDocumentId(),
212                 childCursor2.getString(childCursor.getColumnIndexOrThrow(
213                         Document.COLUMN_DOCUMENT_ID)));
214         assertFalse(childCursor2.moveToNext());
215     }
216 
217     @Test
testQueryChildDocument_EmptyDirs()218     public void testQueryChildDocument_EmptyDirs()
219             throws IOException, CompressorException, ArchiveException {
220         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.empty_dirs));
221         final Cursor cursor = mArchive.queryChildDocuments(
222                 createArchiveId("/").toDocumentId(), null, null);
223 
224         assertTrue(cursor.moveToFirst());
225         assertEquals(
226                 createArchiveId("/dir1/").toDocumentId(),
227                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
228         assertEquals("dir1",
229                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
230         assertEquals(Document.MIME_TYPE_DIR,
231                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
232         assertEquals(0,
233                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
234         assertFalse(cursor.moveToNext());
235 
236         final Cursor childCursor = mArchive.queryChildDocuments(
237                 createArchiveId("/dir1/").toDocumentId(), null, null);
238 
239         assertRowExist(childCursor, createArchiveId("/dir1/dir2/").toDocumentId());
240         assertEquals("dir2",
241                 childCursor.getString(childCursor.getColumnIndexOrThrow(
242                         Document.COLUMN_DISPLAY_NAME)));
243         assertEquals(Document.MIME_TYPE_DIR,
244                 childCursor.getString(childCursor.getColumnIndexOrThrow(
245                         Document.COLUMN_MIME_TYPE)));
246         assertEquals(0,
247                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
248 
249         assertRowExist(childCursor, createArchiveId("/dir1/dir3/").toDocumentId());
250         assertEquals(
251                 createArchiveId("/dir1/dir3/").toDocumentId(),
252                 childCursor.getString(childCursor.getColumnIndexOrThrow(
253                         Document.COLUMN_DOCUMENT_ID)));
254         assertEquals("dir3",
255                 childCursor.getString(childCursor.getColumnIndexOrThrow(
256                         Document.COLUMN_DISPLAY_NAME)));
257         assertEquals(Document.MIME_TYPE_DIR,
258                 childCursor.getString(childCursor.getColumnIndexOrThrow(
259                         Document.COLUMN_MIME_TYPE)));
260         assertEquals(0,
261                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
262 
263         final Cursor childCursor2 = mArchive.queryChildDocuments(
264                 createArchiveId("/dir1/dir2/").toDocumentId(),
265                 null, null);
266         assertFalse(childCursor2.moveToFirst());
267 
268         final Cursor childCursor3 = mArchive.queryChildDocuments(
269                 createArchiveId("/dir1/dir3/").toDocumentId(),
270                 null, null);
271         assertFalse(childCursor3.moveToFirst());
272     }
273 
274     @Test
testGetDocumentType()275     public void testGetDocumentType() throws IOException, CompressorException, ArchiveException {
276         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
277         assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
278                 createArchiveId("/dir1/").toDocumentId()));
279         assertEquals("text/plain", mArchive.getDocumentType(
280                 createArchiveId("/file1.txt").toDocumentId()));
281     }
282 
283     @Test
testIsChildDocument()284     public void testIsChildDocument() throws IOException, CompressorException, ArchiveException {
285         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
286         final String documentId = createArchiveId("/").toDocumentId();
287         assertTrue(mArchive.isChildDocument(documentId,
288                 createArchiveId("/dir1/").toDocumentId()));
289         assertFalse(mArchive.isChildDocument(documentId,
290                 createArchiveId("/this-does-not-exist").toDocumentId()));
291         assertTrue(mArchive.isChildDocument(
292                 createArchiveId("/dir1/").toDocumentId(),
293                 createArchiveId("/dir1/cherries.txt").toDocumentId()));
294         assertTrue(mArchive.isChildDocument(documentId,
295                 createArchiveId("/dir1/cherries.txt").toDocumentId()));
296     }
297 
298     @Test
testQueryDocument()299     public void testQueryDocument() throws IOException, CompressorException, ArchiveException {
300         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
301         final Cursor cursor = mArchive.queryDocument(
302                 createArchiveId("/dir2/strawberries.txt").toDocumentId(),
303                 null);
304 
305         assertTrue(cursor.moveToFirst());
306         assertEquals(
307                 createArchiveId("/dir2/strawberries.txt").toDocumentId(),
308                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
309         assertEquals("strawberries.txt",
310                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
311         assertEquals("text/plain",
312                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
313         assertEquals(21,
314                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
315     }
316 
queryDocumentByResIdWithMimeTypeAndVerify(@dRes int resId, String mimeType)317     private void queryDocumentByResIdWithMimeTypeAndVerify(@IdRes int resId, String mimeType)
318             throws IOException, CompressorException, ArchiveException {
319         loadArchive(mTestUtils.getSeekableDescriptor(resId),
320                 mimeType);
321         final String documentId = createArchiveId("/hello/hello.txt").toDocumentId();
322 
323         final Cursor cursor = mArchive.queryDocument(documentId, null);
324         cursor.moveToNext();
325 
326         assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))
327                 .isEqualTo(documentId);
328         assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)))
329                 .isEqualTo("hello.txt");
330         assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)))
331                 .isEqualTo("text/plain");
332         assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)))
333                 .isEqualTo(48);
334     }
335 
336     @Test
archive_sevenZFile_containsList()337     public void archive_sevenZFile_containsList()
338             throws IOException, CompressorException, ArchiveException {
339         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_7z,
340                 "application/x-7z-compressed");
341     }
342 
343     @Test
archive_tar_containsList()344     public void archive_tar_containsList()
345             throws IOException, CompressorException, ArchiveException {
346         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar, "application/x-tar");
347     }
348 
349     @Test
archive_tgz_containsList()350     public void archive_tgz_containsList()
351             throws IOException, CompressorException, ArchiveException {
352         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tgz,
353                 "application/x-compressed-tar");
354     }
355 
356     @Test
archive_tarXz_containsList()357     public void archive_tarXz_containsList()
358             throws IOException, CompressorException, ArchiveException {
359         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_xz,
360                 "application/x-xz-compressed-tar");
361     }
362 
363     @Test
archive_tarBz_containsList()364     public void archive_tarBz_containsList()
365             throws IOException, CompressorException, ArchiveException {
366         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_bz2,
367                 "application/x-bzip-compressed-tar");
368     }
369 
370     @Test
archive_tarBrotli_containsList()371     public void archive_tarBrotli_containsList()
372             throws IOException, CompressorException, ArchiveException {
373         queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_br,
374                 "application/x-brotli-compressed-tar");
375     }
376 
377     @Test
testOpenDocument()378     public void testOpenDocument()
379             throws IOException, CompressorException, ArchiveException, ErrnoException {
380         loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive));
381         commonTestOpenDocument();
382     }
383 
384     @Test
testOpenDocument_NonSeekable()385     public void testOpenDocument_NonSeekable()
386             throws IOException, CompressorException, ArchiveException, ErrnoException {
387         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
388         commonTestOpenDocument();
389     }
390 
391     // Common part of testOpenDocument and testOpenDocument_NonSeekable.
commonTestOpenDocument()392     void commonTestOpenDocument() throws IOException, ErrnoException {
393         final ParcelFileDescriptor descriptor = mArchive.openDocument(
394                 createArchiveId("/dir2/strawberries.txt").toDocumentId(),
395                 "r", null /* signal */);
396         assertTrue(Archive.canSeek(descriptor));
397         try (ParcelFileDescriptor.AutoCloseInputStream inputStream =
398                 new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) {
399             Os.lseek(descriptor.getFileDescriptor(), "I love ".length(), OsConstants.SEEK_SET);
400             assertEquals("strawberries!", new Scanner(inputStream).nextLine());
401             Os.lseek(descriptor.getFileDescriptor(), 0, OsConstants.SEEK_SET);
402             assertEquals("I love strawberries!", new Scanner(inputStream).nextLine());
403         }
404     }
405 
406     @Test
testCanSeek()407     public void testCanSeek() throws IOException {
408         assertTrue(Archive.canSeek(mTestUtils.getSeekableDescriptor(R.raw.archive)));
409         assertFalse(Archive.canSeek(mTestUtils.getNonSeekableDescriptor(R.raw.archive)));
410     }
411 
412     @Test
testBrokenArchive()413     public void testBrokenArchive() throws IOException, CompressorException, ArchiveException {
414         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
415         final Cursor cursor = mArchive.queryChildDocuments(
416                 createArchiveId("/").toDocumentId(), null, null);
417     }
418 }
419