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 java.io.IOException;
42 import java.util.Scanner;
43 import java.util.concurrent.ExecutorService;
44 import java.util.concurrent.Executors;
45 import java.util.concurrent.TimeUnit;
46 
47 import org.apache.commons.compress.archivers.ArchiveException;
48 import org.apache.commons.compress.compressors.CompressorException;
49 import org.junit.After;
50 import org.junit.Before;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 
54 @RunWith(AndroidJUnit4.class)
55 @MediumTest
56 public class ReadableArchiveTest {
57     private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
58     private static final String NOTIFICATION_URI =
59             "content://com.android.documentsui.archives/notification-uri";
60     private ExecutorService mExecutor = null;
61     private Archive mArchive = null;
62     private TestUtils mTestUtils = null;
63 
64     @Before
setUp()65     public void setUp() throws Exception {
66         mExecutor = Executors.newSingleThreadExecutor();
67         mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
68                 InstrumentationRegistry.getContext(), mExecutor);
69     }
70 
71     @After
tearDown()72     public void tearDown() throws Exception {
73         mExecutor.shutdown();
74         assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
75         if (mArchive != null) {
76             mArchive.close();
77         }
78     }
79 
createArchiveId(String path)80     public static ArchiveId createArchiveId(String path) {
81         return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_READ_ONLY, path);
82     }
83 
loadArchive(ParcelFileDescriptor descriptor, String mimeType)84     private void loadArchive(ParcelFileDescriptor descriptor, String mimeType)
85             throws IOException, CompressorException, ArchiveException {
86         mArchive = ReadableArchive.createForParcelFileDescriptor(
87                 InstrumentationRegistry.getTargetContext(),
88                 descriptor,
89                 ARCHIVE_URI,
90                 mimeType,
91                 ParcelFileDescriptor.MODE_READ_ONLY,
92                 Uri.parse(NOTIFICATION_URI));
93     }
94 
loadArchive(ParcelFileDescriptor descriptor)95     private void loadArchive(ParcelFileDescriptor descriptor)
96             throws IOException, CompressorException, ArchiveException {
97         loadArchive(descriptor, "application/zip");
98     }
99 
assertRowExist(Cursor cursor, String targetDocId)100     private static void assertRowExist(Cursor cursor, String targetDocId) {
101         assertTrue(cursor.moveToFirst());
102 
103         boolean found = false;
104         final int count = cursor.getCount();
105         for (int i = 0; i < count; i++) {
106             cursor.moveToPosition(i);
107             if (TextUtils.equals(targetDocId, cursor.getString(
108                     cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))) {
109                 found = true;
110                 break;
111             }
112         }
113 
114         assertTrue(targetDocId + " should be exists", found);
115     }
116 
117     @Test
testQueryChildDocument()118     public void testQueryChildDocument()
119             throws IOException, CompressorException, ArchiveException {
120         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
121         final Cursor cursor = mArchive.queryChildDocuments(
122                 createArchiveId("/").toDocumentId(), null, null);
123 
124         assertRowExist(cursor, createArchiveId("/file1.txt").toDocumentId());
125         assertEquals("file1.txt",
126                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
127         assertEquals("text/plain",
128                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
129         assertEquals(13,
130                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
131 
132         assertRowExist(cursor, createArchiveId("/dir1/").toDocumentId());
133         assertEquals("dir1",
134                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
135         assertEquals(Document.MIME_TYPE_DIR,
136                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
137         assertEquals(0,
138                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
139 
140         assertRowExist(cursor, createArchiveId("/dir2/").toDocumentId());
141         assertEquals("dir2",
142                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
143         assertEquals(Document.MIME_TYPE_DIR,
144                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
145         assertEquals(0,
146                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
147 
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 (final 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