1/*
2 * Copyright (C) 2013 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
17package android.support.v4.content;
18
19import static android.provider.OpenableColumns.DISPLAY_NAME;
20import static android.provider.OpenableColumns.SIZE;
21
22import static org.junit.Assert.assertEquals;
23import static org.junit.Assert.fail;
24
25import android.content.ContentResolver;
26import android.content.Context;
27import android.database.Cursor;
28import android.net.Uri;
29import android.os.Environment;
30import android.support.test.InstrumentationRegistry;
31import android.support.test.filters.SmallTest;
32import android.support.test.runner.AndroidJUnit4;
33import android.support.v4.content.FileProvider.SimplePathStrategy;
34import android.test.MoreAsserts;
35
36import org.junit.Before;
37import org.junit.Test;
38import org.junit.runner.RunWith;
39
40import java.io.ByteArrayOutputStream;
41import java.io.File;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.IOException;
45import java.io.InputStream;
46import java.io.OutputStream;
47
48/**
49 * Tests for {@link FileProvider}
50 */
51@SmallTest
52@RunWith(AndroidJUnit4.class)
53public class FileProviderTest {
54    private static final String TEST_AUTHORITY = "moocow";
55
56    private static final String TEST_FILE = "file.test";
57    private static final byte[] TEST_DATA = new byte[] { (byte) 0xf0, 0x00, 0x0d };
58    private static final byte[] TEST_DATA_ALT = new byte[] { (byte) 0x33, 0x66 };
59
60    private ContentResolver mResolver;
61    private Context mContext;
62
63    @Before
64    public void setup() throws Exception {
65        mContext = InstrumentationRegistry.getTargetContext();
66        mResolver = mContext.getContentResolver();
67    }
68
69    @Test
70    public void testStrategyUriSimple() throws Exception {
71        final SimplePathStrategy strat = new SimplePathStrategy("authority");
72        strat.addRoot("tag", mContext.getFilesDir());
73
74        File file = buildPath(mContext.getFilesDir(), "file.test");
75        assertEquals("content://authority/tag/file.test",
76                strat.getUriForFile(file).toString());
77
78        file = buildPath(mContext.getFilesDir(), "subdir", "file.test");
79        assertEquals("content://authority/tag/subdir/file.test",
80                strat.getUriForFile(file).toString());
81
82        file = buildPath(Environment.getExternalStorageDirectory(), "file.test");
83        try {
84            strat.getUriForFile(file);
85            fail("somehow got uri for file outside roots?");
86        } catch (IllegalArgumentException e) {
87        }
88    }
89
90    @Test
91    public void testStrategyUriJumpOutside() throws Exception {
92        final SimplePathStrategy strat = new SimplePathStrategy("authority");
93        strat.addRoot("tag", mContext.getFilesDir());
94
95        File file = buildPath(mContext.getFilesDir(), "..", "file.test");
96        try {
97            strat.getUriForFile(file);
98            fail("file escaped!");
99        } catch (IllegalArgumentException e) {
100        }
101    }
102
103    @Test
104    public void testStrategyUriShortestRoot() throws Exception {
105        SimplePathStrategy strat = new SimplePathStrategy("authority");
106        strat.addRoot("tag1", mContext.getFilesDir());
107        strat.addRoot("tag2", new File("/"));
108
109        File file = buildPath(mContext.getFilesDir(), "file.test");
110        assertEquals("content://authority/tag1/file.test",
111                strat.getUriForFile(file).toString());
112
113        strat = new SimplePathStrategy("authority");
114        strat.addRoot("tag1", new File("/"));
115        strat.addRoot("tag2", mContext.getFilesDir());
116
117        file = buildPath(mContext.getFilesDir(), "file.test");
118        assertEquals("content://authority/tag2/file.test",
119                strat.getUriForFile(file).toString());
120    }
121
122    @Test
123    public void testStrategyFileSimple() throws Exception {
124        final SimplePathStrategy strat = new SimplePathStrategy("authority");
125        strat.addRoot("tag", mContext.getFilesDir());
126
127        File expectedRoot = mContext.getFilesDir().getCanonicalFile();
128        File file = buildPath(expectedRoot, "file.test");
129        assertEquals(file.getPath(),
130                strat.getFileForUri(Uri.parse("content://authority/tag/file.test")).getPath());
131
132        file = buildPath(expectedRoot, "subdir", "file.test");
133        assertEquals(file.getPath(), strat.getFileForUri(
134                Uri.parse("content://authority/tag/subdir/file.test")).getPath());
135    }
136
137    @Test
138    public void testStrategyFileJumpOutside() throws Exception {
139        final SimplePathStrategy strat = new SimplePathStrategy("authority");
140        strat.addRoot("tag", mContext.getFilesDir());
141
142        try {
143            strat.getFileForUri(Uri.parse("content://authority/tag/../file.test"));
144            fail("file escaped!");
145        } catch (SecurityException e) {
146        }
147    }
148
149    @Test
150    public void testStrategyEscaping() throws Exception {
151        final SimplePathStrategy strat = new SimplePathStrategy("authority");
152        strat.addRoot("t/g", mContext.getFilesDir());
153
154        File expectedRoot = mContext.getFilesDir().getCanonicalFile();
155        File file = buildPath(expectedRoot, "lol\"wat?foo&bar", "wat.txt");
156        final String expected = "content://authority/t%2Fg/lol%22wat%3Ffoo%26bar/wat.txt";
157
158        assertEquals(expected,
159                strat.getUriForFile(file).toString());
160        assertEquals(file.getPath(),
161                strat.getFileForUri(Uri.parse(expected)).getPath());
162    }
163
164    @Test
165    public void testStrategyExtraParams() throws Exception {
166        final SimplePathStrategy strat = new SimplePathStrategy("authority");
167        strat.addRoot("tag", mContext.getFilesDir());
168
169        File expectedRoot = mContext.getFilesDir().getCanonicalFile();
170        File file = buildPath(expectedRoot, "file.txt");
171        assertEquals(file.getPath(), strat.getFileForUri(
172                Uri.parse("content://authority/tag/file.txt?extra=foo")).getPath());
173    }
174
175    @Test
176    public void testStrategyExtraSeparators() throws Exception {
177        final SimplePathStrategy strat = new SimplePathStrategy("authority");
178        strat.addRoot("tag", mContext.getFilesDir());
179
180        // When canonicalized, the path separators are trimmed
181        File inFile = new File(mContext.getFilesDir(), "//foo//bar//");
182        File expectedRoot = mContext.getFilesDir().getCanonicalFile();
183        File outFile = new File(expectedRoot, "/foo/bar");
184        final String expected = "content://authority/tag/foo/bar";
185
186        assertEquals(expected,
187                strat.getUriForFile(inFile).toString());
188        assertEquals(outFile.getPath(),
189                strat.getFileForUri(Uri.parse(expected)).getPath());
190    }
191
192    @Test
193    public void testQueryProjectionNull() throws Exception {
194        final File file = new File(mContext.getFilesDir(), TEST_FILE);
195        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
196
197        // Verify that null brings out default columns
198        Cursor cursor = mResolver.query(uri, null, null, null, null);
199        try {
200            assertEquals(1, cursor.getCount());
201            cursor.moveToFirst();
202            assertEquals(TEST_FILE, cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)));
203            assertEquals(TEST_DATA.length, cursor.getLong(cursor.getColumnIndex(SIZE)));
204        } finally {
205            cursor.close();
206        }
207    }
208
209    @Test
210    public void testQueryProjectionOrder() throws Exception {
211        final File file = new File(mContext.getFilesDir(), TEST_FILE);
212        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
213
214        // Verify that swapped order works
215        Cursor cursor = mResolver.query(uri, new String[] {
216                SIZE, DISPLAY_NAME }, null, null, null);
217        try {
218            assertEquals(1, cursor.getCount());
219            cursor.moveToFirst();
220            assertEquals(TEST_DATA.length, cursor.getLong(0));
221            assertEquals(TEST_FILE, cursor.getString(1));
222        } finally {
223            cursor.close();
224        }
225
226        cursor = mResolver.query(uri, new String[] {
227                DISPLAY_NAME, SIZE }, null, null, null);
228        try {
229            assertEquals(1, cursor.getCount());
230            cursor.moveToFirst();
231            assertEquals(TEST_FILE, cursor.getString(0));
232            assertEquals(TEST_DATA.length, cursor.getLong(1));
233        } finally {
234            cursor.close();
235        }
236    }
237
238    @Test
239    public void testQueryExtraColumn() throws Exception {
240        final File file = new File(mContext.getFilesDir(), TEST_FILE);
241        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
242
243        // Verify that extra column doesn't gook things up
244        Cursor cursor = mResolver.query(uri, new String[] {
245                SIZE, "foobar", DISPLAY_NAME }, null, null, null);
246        try {
247            assertEquals(1, cursor.getCount());
248            cursor.moveToFirst();
249            assertEquals(TEST_DATA.length, cursor.getLong(0));
250            assertEquals(TEST_FILE, cursor.getString(1));
251        } finally {
252            cursor.close();
253        }
254    }
255
256    @Test
257    public void testReadFile() throws Exception {
258        final File file = new File(mContext.getFilesDir(), TEST_FILE);
259        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
260
261        assertContentsEquals(TEST_DATA, uri);
262    }
263
264    @Test
265    public void testWriteFile() throws Exception {
266        final File file = new File(mContext.getFilesDir(), TEST_FILE);
267        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
268
269        assertContentsEquals(TEST_DATA, uri);
270
271        final OutputStream out = mResolver.openOutputStream(uri);
272        try {
273            out.write(TEST_DATA_ALT);
274        } finally {
275            closeQuietly(out);
276        }
277
278        assertContentsEquals(TEST_DATA_ALT, uri);
279    }
280
281    @Test
282    public void testWriteMissingFile() throws Exception {
283        final File file = new File(mContext.getFilesDir(), TEST_FILE);
284        final Uri uri = stageFileAndGetUri(file, null);
285
286        try {
287            assertContentsEquals(new byte[0], uri);
288            fail("Somehow read missing file?");
289        } catch(FileNotFoundException e) {
290        }
291
292        final OutputStream out = mResolver.openOutputStream(uri);
293        try {
294            out.write(TEST_DATA_ALT);
295        } finally {
296            closeQuietly(out);
297        }
298
299        assertContentsEquals(TEST_DATA_ALT, uri);
300    }
301
302    @Test
303    public void testDelete() throws Exception {
304        final File file = new File(mContext.getFilesDir(), TEST_FILE);
305        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
306
307        assertContentsEquals(TEST_DATA, uri);
308
309        assertEquals(1, mResolver.delete(uri, null, null));
310        assertEquals(0, mResolver.delete(uri, null, null));
311
312        try {
313            assertContentsEquals(new byte[0], uri);
314            fail("Somehow read missing file?");
315        } catch(FileNotFoundException e) {
316        }
317    }
318
319    @Test
320    public void testMetaDataTargets() {
321        Uri actual;
322
323        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
324                new File("/proc/version"));
325        assertEquals("content://moocow/test_root/proc/version", actual.toString());
326
327        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
328                new File("/proc/1/mountinfo"));
329        assertEquals("content://moocow/test_init/mountinfo", actual.toString());
330
331        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
332                buildPath(mContext.getFilesDir(), "meow"));
333        assertEquals("content://moocow/test_files/meow", actual.toString());
334
335        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
336                buildPath(mContext.getFilesDir(), "thumbs", "rawr"));
337        assertEquals("content://moocow/test_thumbs/rawr", actual.toString());
338
339        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
340                buildPath(mContext.getCacheDir(), "up", "down"));
341        assertEquals("content://moocow/test_cache/up/down", actual.toString());
342
343        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
344                buildPath(Environment.getExternalStorageDirectory(), "Android", "obb", "foobar"));
345        assertEquals("content://moocow/test_external/Android/obb/foobar", actual.toString());
346
347        File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(mContext, null);
348        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
349            buildPath(externalFilesDirs[0], "foo", "bar"));
350        assertEquals("content://moocow/test_external_files/foo/bar", actual.toString());
351
352        File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(mContext);
353        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
354            buildPath(externalCacheDirs[0], "foo", "bar"));
355        assertEquals("content://moocow/test_external_cache/foo/bar", actual.toString());
356    }
357
358    private void assertContentsEquals(byte[] expected, Uri actual) throws Exception {
359        final InputStream in = mResolver.openInputStream(actual);
360        try {
361            MoreAsserts.assertEquals(expected, readFully(in));
362        } finally {
363            closeQuietly(in);
364        }
365    }
366
367    private Uri stageFileAndGetUri(File file, byte[] data) throws Exception {
368        if (data != null) {
369            final FileOutputStream out = new FileOutputStream(file);
370            try {
371                out.write(data);
372            } finally {
373                out.close();
374            }
375        } else {
376            file.delete();
377        }
378        return FileProvider.getUriForFile(mContext, TEST_AUTHORITY, file);
379    }
380
381    private static File buildPath(File base, String... segments) {
382        File cur = base;
383        for (String segment : segments) {
384            if (cur == null) {
385                cur = new File(segment);
386            } else {
387                cur = new File(cur, segment);
388            }
389        }
390        return cur;
391    }
392
393    /**
394     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
395     */
396    private static void closeQuietly(AutoCloseable closeable) {
397        if (closeable != null) {
398            try {
399                closeable.close();
400            } catch (RuntimeException rethrown) {
401                throw rethrown;
402            } catch (Exception ignored) {
403            }
404        }
405    }
406
407    /**
408     * Returns a byte[] containing the remainder of 'in', closing it when done.
409     */
410    private static byte[] readFully(InputStream in) throws IOException {
411        try {
412            return readFullyNoClose(in);
413        } finally {
414            in.close();
415        }
416    }
417
418    /**
419     * Returns a byte[] containing the remainder of 'in'.
420     */
421    private static byte[] readFullyNoClose(InputStream in) throws IOException {
422        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
423        byte[] buffer = new byte[1024];
424        int count;
425        while ((count = in.read(buffer)) != -1) {
426            bytes.write(buffer, 0, count);
427        }
428        return bytes.toByteArray();
429    }
430}
431