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.xmlpull.v1.XmlPullParser.END_DOCUMENT; 20 import static org.xmlpull.v1.XmlPullParser.END_TAG; 21 import static org.xmlpull.v1.XmlPullParser.START_TAG; 22 23 import android.media.ExifInterface; 24 import android.text.TextUtils; 25 import android.util.Log; 26 import android.util.Xml; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.annotation.VisibleForTesting; 31 32 import com.android.providers.media.MediaProvider; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import java.io.ByteArrayInputStream; 38 import java.io.File; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.nio.charset.StandardCharsets; 42 import java.nio.file.Files; 43 import java.util.Arrays; 44 import java.util.Set; 45 import java.util.UUID; 46 47 /** 48 * Parser for Extensible Metadata Platform (XMP) metadata. Designed to mirror 49 * ergonomics of {@link ExifInterface}. 50 * <p> 51 * Since values can be repeated multiple times within the same XMP data, this 52 * parser prefers the first valid definition of a specific value, and it ignores 53 * any subsequent attempts to redefine that value. 54 */ 55 public class XmpInterface { 56 private static final String TAG = "XmpInterface"; 57 private static final String NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 58 private static final String NS_XMP = "http://ns.adobe.com/xap/1.0/"; 59 private static final String NS_XMPMM = "http://ns.adobe.com/xap/1.0/mm/"; 60 private static final String NS_DC = "http://purl.org/dc/elements/1.1/"; 61 private static final String NS_EXIF = "http://ns.adobe.com/exif/1.0/"; 62 63 private static final String NAME_DESCRIPTION = "Description"; 64 private static final String NAME_FORMAT = "format"; 65 private static final String NAME_DOCUMENT_ID = "DocumentID"; 66 private static final String NAME_ORIGINAL_DOCUMENT_ID = "OriginalDocumentID"; 67 private static final String NAME_INSTANCE_ID = "InstanceID"; 68 69 private final LongArray mRedactedRanges = new LongArray(); 70 private @NonNull byte[] mRedactedXmp; 71 private String mFormat; 72 private String mDocumentId; 73 private String mInstanceId; 74 private String mOriginalDocumentId; 75 XmpInterface(@onNull byte[] rawXmp, @NonNull Set<String> redactedExifTags, @NonNull long[] xmpOffsets)76 private XmpInterface(@NonNull byte[] rawXmp, @NonNull Set<String> redactedExifTags, 77 @NonNull long[] xmpOffsets) throws IOException { 78 mRedactedXmp = rawXmp; 79 80 final ByteCountingInputStream in = new ByteCountingInputStream( 81 new ByteArrayInputStream(rawXmp)); 82 final long xmpOffset = xmpOffsets.length == 0 ? 0 : xmpOffsets[0]; 83 try { 84 final XmlPullParser parser = Xml.newPullParser(); 85 parser.setInput(in, StandardCharsets.UTF_8.name()); 86 87 long offset = 0; 88 int type; 89 while ((type = parser.next()) != END_DOCUMENT) { 90 if (type != START_TAG) { 91 offset = in.getOffset(parser); 92 continue; 93 } 94 95 // The values we're interested in could be stored in either 96 // attributes or tags, so we're willing to look for both 97 98 final String ns = parser.getNamespace(); 99 final String name = parser.getName(); 100 101 if (NS_RDF.equals(ns) && NAME_DESCRIPTION.equals(name)) { 102 mFormat = maybeOverride(mFormat, 103 parser.getAttributeValue(NS_DC, NAME_FORMAT)); 104 mDocumentId = maybeOverride(mDocumentId, 105 parser.getAttributeValue(NS_XMPMM, NAME_DOCUMENT_ID)); 106 mInstanceId = maybeOverride(mInstanceId, 107 parser.getAttributeValue(NS_XMPMM, NAME_INSTANCE_ID)); 108 mOriginalDocumentId = maybeOverride(mOriginalDocumentId, 109 parser.getAttributeValue(NS_XMPMM, NAME_ORIGINAL_DOCUMENT_ID)); 110 } else if (NS_DC.equals(ns) && NAME_FORMAT.equals(name)) { 111 mFormat = maybeOverride(mFormat, parser.nextText()); 112 } else if (NS_XMPMM.equals(ns) && NAME_DOCUMENT_ID.equals(name)) { 113 mDocumentId = maybeOverride(mDocumentId, parser.nextText()); 114 } else if (NS_XMPMM.equals(ns) && NAME_INSTANCE_ID.equals(name)) { 115 mInstanceId = maybeOverride(mInstanceId, parser.nextText()); 116 } else if (NS_XMPMM.equals(ns) && NAME_ORIGINAL_DOCUMENT_ID.equals(name)) { 117 mOriginalDocumentId = maybeOverride(mOriginalDocumentId, parser.nextText()); 118 } else if (NS_EXIF.equals(ns) && redactedExifTags.contains(name)) { 119 long start = offset; 120 do { 121 type = parser.next(); 122 } while (type != END_TAG || !parser.getName().equals(name)); 123 offset = in.getOffset(parser); 124 125 // Redact range within entire file 126 mRedactedRanges.add(xmpOffset + start); 127 mRedactedRanges.add(xmpOffset + offset); 128 129 // Redact range within local copy 130 Arrays.fill(mRedactedXmp, (int) start, (int) offset, (byte) ' '); 131 } 132 } 133 } catch (XmlPullParserException e) { 134 throw new IOException(e); 135 } catch (OutOfMemoryError e) { 136 Log.w(TAG, "Couldn't read large xmp", e); 137 throw new IOException(e); 138 } 139 } 140 fromContainer(@onNull InputStream is)141 public static @NonNull XmpInterface fromContainer(@NonNull InputStream is) 142 throws IOException { 143 return fromContainer(new ExifInterface(is)); 144 } 145 fromContainer(@onNull InputStream is, @NonNull Set<String> redactedExifTags)146 public static @NonNull XmpInterface fromContainer(@NonNull InputStream is, 147 @NonNull Set<String> redactedExifTags) throws IOException { 148 return fromContainer(new ExifInterface(is), redactedExifTags); 149 } 150 fromContainer(@onNull ExifInterface exif)151 public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif) 152 throws IOException { 153 return fromContainer(exif, MediaProvider.sRedactedExifTags); 154 } 155 fromContainer(@onNull ExifInterface exif, @NonNull Set<String> redactedExifTags)156 public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif, 157 @NonNull Set<String> redactedExifTags) throws IOException { 158 final byte[] buf; 159 long[] xmpOffsets; 160 if (exif.hasAttribute(ExifInterface.TAG_XMP)) { 161 buf = exif.getAttributeBytes(ExifInterface.TAG_XMP); 162 xmpOffsets = exif.getAttributeRange(ExifInterface.TAG_XMP); 163 } else { 164 buf = new byte[0]; 165 xmpOffsets = new long[0]; 166 } 167 return new XmpInterface(buf, redactedExifTags, xmpOffsets); 168 } 169 fromContainer(@onNull IsoInterface iso)170 public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso) 171 throws IOException { 172 return fromContainer(iso, MediaProvider.sRedactedExifTags); 173 } 174 fromContainer(@onNull IsoInterface iso, @NonNull Set<String> redactedExifTags)175 public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso, 176 @NonNull Set<String> redactedExifTags) throws IOException { 177 byte[] buf = null; 178 long[] xmpOffsets = new long[0]; 179 if (buf == null) { 180 UUID uuid = UUID.fromString("be7acfcb-97a9-42e8-9c71-999491e3afac"); 181 buf = iso.getBoxBytes(uuid); 182 xmpOffsets = iso.getBoxRanges(uuid); 183 } 184 if (buf == null) { 185 buf = iso.getBoxBytes(IsoInterface.BOX_XMP); 186 xmpOffsets = iso.getBoxRanges(IsoInterface.BOX_XMP); 187 } 188 if (buf == null) { 189 buf = new byte[0]; 190 xmpOffsets = new long[0]; 191 } 192 return new XmpInterface(buf, redactedExifTags, xmpOffsets); 193 } 194 fromSidecar(@onNull File file)195 public static @NonNull XmpInterface fromSidecar(@NonNull File file) 196 throws IOException { 197 return new XmpInterface(Files.readAllBytes(file.toPath()), 198 MediaProvider.sRedactedExifTags, new long[0]); 199 } 200 maybeOverride(@ullable String existing, @Nullable String current)201 private static @Nullable String maybeOverride(@Nullable String existing, 202 @Nullable String current) { 203 if (!TextUtils.isEmpty(existing)) { 204 // If already defined, first definition always wins 205 return existing; 206 } else if (!TextUtils.isEmpty(current)) { 207 // If current defined, it wins 208 return current; 209 } else { 210 // Otherwise, null wins to prevent weird empty strings 211 return null; 212 } 213 } 214 getFormat()215 public @Nullable String getFormat() { 216 return mFormat; 217 } 218 getDocumentId()219 public @Nullable String getDocumentId() { 220 return mDocumentId; 221 } 222 getInstanceId()223 public @Nullable String getInstanceId() { 224 return mInstanceId; 225 } 226 getOriginalDocumentId()227 public @Nullable String getOriginalDocumentId() { 228 return mOriginalDocumentId; 229 } 230 getRedactedXmp()231 public @NonNull byte[] getRedactedXmp() { 232 return mRedactedXmp; 233 } 234 235 /** The [start, end] offsets in the original file where to-be redacted info is stored */ getRedactionRanges()236 public LongArray getRedactionRanges() { 237 return mRedactedRanges; 238 } 239 240 @VisibleForTesting 241 public static class ByteCountingInputStream extends InputStream { 242 private final InputStream mWrapped; 243 private final LongArray mOffsets; 244 private int mLine; 245 private int mOffset; 246 ByteCountingInputStream(InputStream wrapped)247 public ByteCountingInputStream(InputStream wrapped) { 248 mWrapped = wrapped; 249 mOffsets = new LongArray(); 250 mLine = 1; 251 mOffset = 0; 252 } 253 getOffset(XmlPullParser parser)254 public long getOffset(XmlPullParser parser) { 255 int line = parser.getLineNumber() - 1; // getLineNumber is 1-based 256 long lineOffset = line == 0 ? 0 : mOffsets.get(line - 1); 257 int columnOffset = parser.getColumnNumber() - 1; // meant to be 0-based, but is 1-based? 258 return lineOffset + columnOffset; 259 } 260 261 @Override read(byte[] b)262 public int read(byte[] b) throws IOException { 263 return read(b, 0, b.length); 264 } 265 266 @Override read(byte[] b, int off, int len)267 public int read(byte[] b, int off, int len) throws IOException { 268 final int read = mWrapped.read(b, off, len); 269 if (read == -1) return -1; 270 271 for (int i = 0; i < read; i++) { 272 if (b[off + i] == '\n') { 273 mOffsets.add(mLine - 1, mOffset + i + 1); 274 mLine++; 275 } 276 } 277 mOffset += read; 278 return read; 279 } 280 281 @Override read()282 public int read() throws IOException { 283 int r = mWrapped.read(); 284 if (r == -1) return -1; 285 286 mOffset++; 287 if (r == '\n') { 288 mOffsets.add(mLine - 1, mOffset); 289 mLine++; 290 } 291 return r; 292 } 293 294 @Override skip(long n)295 public long skip(long n) throws IOException { 296 return super.skip(n); 297 } 298 299 @Override available()300 public int available() throws IOException { 301 return mWrapped.available(); 302 } 303 304 @Override close()305 public void close() throws IOException { 306 mWrapped.close(); 307 } 308 309 @Override toString()310 public String toString() { 311 return java.util.Arrays.toString(mOffsets.toArray()); 312 } 313 } 314 } 315