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