• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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