1 /*
2  * Copyright (C) 2019 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.providers.media.util;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertNull;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 
27 import android.content.Context;
28 import android.media.ExifInterface;
29 import android.util.ArraySet;
30 import android.util.Xml;
31 
32 import androidx.test.InstrumentationRegistry;
33 import androidx.test.runner.AndroidJUnit4;
34 
35 import com.android.providers.media.R;
36 
37 import com.google.common.truth.Truth;
38 
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.xmlpull.v1.XmlPullParser;
42 
43 import java.io.ByteArrayInputStream;
44 import java.io.File;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.nio.charset.StandardCharsets;
50 import java.util.Set;
51 
52 @RunWith(AndroidJUnit4.class)
53 public class XmpInterfaceTest {
54     @Test
testContainer_Empty()55     public void testContainer_Empty() throws Exception {
56         final Context context = InstrumentationRegistry.getContext();
57         try (InputStream in = context.getResources().openRawResource(R.raw.test_image)) {
58             final XmpInterface xmp = XmpInterface.fromContainer(in);
59             assertNull(xmp.getFormat());
60             assertNull(xmp.getDocumentId());
61             assertNull(xmp.getInstanceId());
62             assertNull(xmp.getOriginalDocumentId());
63         }
64     }
65 
66     @Test
testContainer_ValidAttrs()67     public void testContainer_ValidAttrs() throws Exception {
68         final Context context = InstrumentationRegistry.getContext();
69         try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) {
70             final XmpInterface xmp = XmpInterface.fromContainer(in);
71             assertEquals("image/dng", xmp.getFormat());
72             assertEquals("xmp.did:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getDocumentId());
73             assertEquals("xmp.iid:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getInstanceId());
74             assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId());
75         }
76     }
77 
78     @Test
testContainer_ValidTags()79     public void testContainer_ValidTags() throws Exception {
80         final Context context = InstrumentationRegistry.getContext();
81         try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_dng)) {
82             final XmpInterface xmp = XmpInterface.fromContainer(in);
83             assertEquals("image/dng", xmp.getFormat());
84             assertEquals("xmp.did:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getDocumentId());
85             assertEquals("xmp.iid:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getInstanceId());
86             assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId());
87         }
88     }
89 
90     @Test
testContainer_ExifRedactionRanges()91     public void testContainer_ExifRedactionRanges() throws Exception {
92         final Set<String> redactionTags = new ArraySet<>();
93         redactionTags.add(ExifInterface.TAG_GPS_LATITUDE);
94         redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE);
95         redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
96         redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
97 
98         final Context context = InstrumentationRegistry.getContext();
99         try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) {
100             ExifInterface exif = new ExifInterface(in);
101             assertEquals(1809, exif.getAttributeRange(ExifInterface.TAG_XMP)[0]);
102             final XmpInterface xmp = XmpInterface.fromContainer(exif, redactionTags);
103 
104             // Confirm redact range within entire file
105             // The XMP contents start at byte 1809. These are the file offsets.
106             final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841};
107             assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
108 
109             // Confirm redact range within local copy
110             final String redactedXmp = new String(xmp.getRedactedXmp());
111             assertFalse(redactedXmp.contains("exif:GPSLatitude"));
112             assertFalse(redactedXmp.contains("exif:GPSLongitude"));
113             assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
114         }
115     }
116 
117     @Test
testContainer_IsoRedactionRanges()118     public void testContainer_IsoRedactionRanges() throws Exception {
119         final Set<String> redactionTags = new ArraySet<>();
120         redactionTags.add(ExifInterface.TAG_GPS_LATITUDE);
121         redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE);
122         redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
123         redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
124 
125         final File file = stageFile(R.raw.test_video_xmp);
126         final IsoInterface mp4 = IsoInterface.fromFile(file);
127         final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags);
128 
129         // Confirm redact range within entire file
130         // The XMP contents start at byte 30286. These are the file offsets.
131         final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515};
132         assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
133 
134         // Confirm redact range within local copy
135         final String redactedXmp = new String(xmp.getRedactedXmp());
136         assertFalse(redactedXmp.contains("exif:GPSLatitude"));
137         assertFalse(redactedXmp.contains("exif:GPSLongitude"));
138         assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
139     }
140 
141     @Test
testContainer_IsoRedactionRanges_BadTagValue()142     public void testContainer_IsoRedactionRanges_BadTagValue() throws Exception {
143         final Set<String> redactionTags = new ArraySet<>();
144         redactionTags.add(ExifInterface.TAG_GPS_LATITUDE);
145         redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE);
146         redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
147         redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
148 
149         // This file has some inner xml in the latitude tag. We should redact anyway.
150         final File file = stageFile(R.raw.test_video_xmp_bad_tag);
151         final IsoInterface mp4 = IsoInterface.fromFile(file);
152         final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags);
153 
154         // The XMP contents start at byte 30286. These are the file offsets.
155         final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515};
156         assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
157     }
158 
159     @Test
testContainer_IsoRedactionRanges_MalformedXml()160     public void testContainer_IsoRedactionRanges_MalformedXml() throws Exception {
161         final Set<String> redactionTags = new ArraySet<>();
162         redactionTags.add(ExifInterface.TAG_GPS_LATITUDE);
163         redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE);
164         redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
165         redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
166 
167         // This file has malformed XML in the latitude tag. XML parsing will fail
168         final File file = stageFile(R.raw.test_video_xmp_malformed);
169         final IsoInterface mp4 = IsoInterface.fromFile(file);
170         try {
171             final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags);
172             fail("Should throw IOException");
173         } catch (IOException e) {}
174     }
175 
176     @Test
testStream_LineOffsets()177     public void testStream_LineOffsets() throws Exception {
178         final String xml =
179                 "<a:b xmlns:a='a' xmlns:c='c' c:d=''\n  c:f='g'>\n  <c:i>j</c:i>\n  </a:b>";
180         final InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
181         final XmpInterface.ByteCountingInputStream stream =
182                 new XmpInterface.ByteCountingInputStream(xmlStream);
183 
184         final long[] expectedElementOffsets = new long[]{46,54,61,70};
185         XmlPullParser parser = Xml.newPullParser();
186         parser.setInput(stream, StandardCharsets.UTF_8.name());
187         int type;
188         int i = 0;
189         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
190             if (type == XmlPullParser.START_TAG || type == XmlPullParser.END_TAG) {
191                 assertEquals(expectedElementOffsets[i++], stream.getOffset(parser));
192             }
193         }
194     }
195 
196     /**
197      * Exercise some random methods for code coverage purposes.
198      */
199     @Test
testStream_Misc()200     public void testStream_Misc() throws Exception {
201         final InputStream xmlStream = new ByteArrayInputStream(
202                 "abcdefghijklmnoprstuvwxyz".getBytes(StandardCharsets.UTF_8));
203         final XmpInterface.ByteCountingInputStream stream =
204                 new XmpInterface.ByteCountingInputStream(xmlStream);
205 
206         {
207             final byte[] buf = new byte[4];
208             stream.read(buf);
209             Truth.assertThat(buf).isEqualTo("abcd".getBytes(StandardCharsets.UTF_8));
210         }
211         {
212             final byte[] buf = new byte[4];
213             stream.read(buf, 0, buf.length);
214             Truth.assertThat(buf).isEqualTo("efgh".getBytes(StandardCharsets.UTF_8));
215         }
216         {
217             assertEquals(4, stream.skip(4));
218             assertEquals((int) 'm', stream.read());
219         }
220 
221         assertNotNull(stream.toString());
222         stream.close();
223     }
224 
stageFile(int resId)225     private static File stageFile(int resId) throws Exception {
226         final Context context = InstrumentationRegistry.getContext();
227         final File file = File.createTempFile("test", ".mp4");
228         try (InputStream in = context.getResources().openRawResource(resId);
229              OutputStream out = new FileOutputStream(file)) {
230             FileUtils.copy(in, out);
231         }
232         return file;
233     }
234 }
235