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