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