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 
17 package android.support.v4.content;
18 
19 import static android.provider.OpenableColumns.DISPLAY_NAME;
20 import static android.provider.OpenableColumns.SIZE;
21 
22 import android.content.ContentResolver;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Environment;
26 import android.support.v4.content.FileProvider.SimplePathStrategy;
27 import android.test.AndroidTestCase;
28 import android.test.MoreAsserts;
29 import android.test.suitebuilder.annotation.Suppress;
30 
31 import java.io.ByteArrayOutputStream;
32 import java.io.File;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 
39 /**
40  * Tests for {@link FileProvider}
41  */
42 @Suppress
43 public 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
setUp()53     protected void setUp() throws Exception {
54         super.setUp();
55 
56         mResolver = getContext().getContentResolver();
57     }
58 
testStrategyUriSimple()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 
testStrategyUriJumpOutside()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 
testStrategyUriShortestRoot()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 
testStrategyFileSimple()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 
testStrategyFileJumpOutside()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 
testStrategyEscaping()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 
testStrategyExtraParams()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 
testStrategyExtraSeparators()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 
testQueryProjectionNull()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 
testQueryProjectionOrder()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 
testQueryExtraColumn()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 
testReadFile()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 
testWriteFile()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 
testWriteMissingFile()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 
testDelete()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 
testMetaDataTargets()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 
assertContentsEquals(byte[] expected, Uri actual)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 
stageFileAndGetUri(File file, byte[] data)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 
buildPath(File base, String... segments)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      */
closeQuietly(AutoCloseable closeable)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      */
readFully(InputStream in)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      */
readFullyNoClose(InputStream in)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