• 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 android.media.ExifInterface;
20 import android.system.ErrnoException;
21 import android.system.Os;
22 import android.system.OsConstants;
23 import android.util.Log;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 
29 import java.io.EOFException;
30 import java.io.File;
31 import java.io.FileDescriptor;
32 import java.io.FileInputStream;
33 import java.io.IOException;
34 import java.nio.ByteOrder;
35 import java.util.ArrayDeque;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Objects;
40 import java.util.Queue;
41 import java.util.UUID;
42 
43 /**
44  * Simple parser for ISO base media file format. Designed to mirror ergonomics
45  * of {@link ExifInterface}.
46  */
47 public class IsoInterface {
48     private static final String TAG = "IsoInterface";
49     private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
50 
51     public static final int BOX_ILST = 0x696c7374;
52     public static final int BOX_FTYP = 0x66747970;
53     public static final int BOX_HDLR = 0x68646c72;
54     public static final int BOX_UUID = 0x75756964;
55     public static final int BOX_META = 0x6d657461;
56     public static final int BOX_XMP = 0x584d505f;
57 
58     public static final int BOX_LOCI = 0x6c6f6369;
59     public static final int BOX_XYZ = 0xa978797a;
60     public static final int BOX_GPS = 0x67707320;
61     public static final int BOX_GPS0 = 0x67707330;
62 
63     /**
64      * Test if given box type is a well-known parent box type.
65      */
isBoxParent(int type)66     private static boolean isBoxParent(int type) {
67         switch (type) {
68             case 0x6d6f6f76: // moov
69             case 0x6d6f6f66: // moof
70             case 0x74726166: // traf
71             case 0x6d667261: // mfra
72             case 0x7472616b: // trak
73             case 0x74726566: // tref
74             case 0x6d646961: // mdia
75             case 0x6d696e66: // minf
76             case 0x64696e66: // dinf
77             case 0x7374626c: // stbl
78             case 0x65647473: // edts
79             case 0x75647461: // udta
80             case 0x6970726f: // ipro
81             case 0x73696e66: // sinf
82             case 0x686e7469: // hnti
83             case 0x68696e66: // hinf
84             case 0x6a703268: // jp2h
85             case 0x696c7374: // ilst
86             case 0x6d657461: // meta
87                 return true;
88             default:
89                 return false;
90         }
91     }
92 
93     /** Top-level boxes */
94     private List<Box> mRoots = new ArrayList<>();
95     /** Flattened view of all boxes */
96     private List<Box> mFlattened = new ArrayList<>();
97 
98     private static class Box {
99         public final int type;
100         public long[] range;
101         public UUID uuid;
102         public byte[] data;
103         public List<Box> children;
104         public int headerSize;
105 
Box(int type, long[] range)106         public Box(int type, long[] range) {
107             this.type = type;
108             this.range = range;
109         }
110     }
111 
112     @VisibleForTesting
typeToString(int type)113     public static String typeToString(int type) {
114         final byte[] buf = new byte[4];
115         Memory.pokeInt(buf, 0, type, ByteOrder.BIG_ENDIAN);
116         return new String(buf);
117     }
118 
readInt(@onNull FileDescriptor fd)119     private static int readInt(@NonNull FileDescriptor fd)
120             throws ErrnoException, IOException {
121         final byte[] buf = new byte[4];
122         if (Os.read(fd, buf, 0, 4) == 4) {
123             return Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
124         } else {
125             throw new EOFException();
126         }
127     }
128 
readUuid(@onNull FileDescriptor fd)129     private static @NonNull UUID readUuid(@NonNull FileDescriptor fd)
130             throws ErrnoException, IOException {
131         final long high = (((long) readInt(fd)) << 32L) | (((long) readInt(fd)) & 0xffffffffL);
132         final long low = (((long) readInt(fd)) << 32L) | (((long) readInt(fd)) & 0xffffffffL);
133         return new UUID(high, low);
134     }
135 
parseNextBox(@onNull FileDescriptor fd, long end, int parentType, @NonNull String prefix)136     private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end, int parentType,
137             @NonNull String prefix) throws ErrnoException, IOException {
138         final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
139 
140         int headerSize = 8;
141         if (end - pos < headerSize) {
142             return null;
143         }
144 
145         long len = Integer.toUnsignedLong(readInt(fd));
146         final int type = readInt(fd);
147 
148         if (len == 0) {
149             // Length 0 means the box extends to the end of the file.
150             len = end - pos;
151         } else if (len == 1) {
152             // Actually 64-bit box length.
153             headerSize += 8;
154             long high = readInt(fd);
155             long low = readInt(fd);
156             len = (high << 32L) | (low & 0xffffffffL);
157         }
158 
159         if (len < headerSize || pos + len > end) {
160             Log.w(TAG, "Invalid box at " + pos + " of length " + len
161                     + ". End of parent " + end);
162             return null;
163         }
164 
165         final Box box = new Box(type, new long[] { pos, len });
166         box.headerSize = headerSize;
167 
168         // Parse UUID box
169         if (type == BOX_UUID) {
170             box.headerSize += 16;
171             box.uuid = readUuid(fd);
172             if (LOGV) {
173                 Log.v(TAG, prefix + "  UUID " + box.uuid);
174             }
175 
176             if (len > Integer.MAX_VALUE) {
177                 Log.w(TAG, "Skipping abnormally large uuid box");
178                 return null;
179             }
180 
181             try {
182                 box.data = new byte[(int) (len - box.headerSize)];
183             } catch (OutOfMemoryError e) {
184                 Log.w(TAG, "Couldn't read large uuid box", e);
185                 return null;
186             }
187             Os.read(fd, box.data, 0, box.data.length);
188         } else if (type == BOX_XMP) {
189             if (len > Integer.MAX_VALUE) {
190                 Log.w(TAG, "Skipping abnormally large xmp box");
191                 return null;
192             }
193 
194             try {
195                 box.data = new byte[(int) (len - box.headerSize)];
196             } catch (OutOfMemoryError e) {
197                 Log.w(TAG, "Couldn't read large xmp box", e);
198                 return null;
199             }
200             Os.read(fd, box.data, 0, box.data.length);
201         } else if (type == BOX_META && len != headerSize) {
202             // The format of this differs in ISO and QT encoding:
203             // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
204             // (qt)  [4 byte size of next atom      ][4 byte hdlr atom type   ]
205             // In case of (iso) we need to skip the next 4 bytes before parsing
206             // the children.
207             readInt(fd);
208             int maybeBoxType = readInt(fd);
209             if (maybeBoxType != BOX_HDLR) {
210                 // ISO, skip 4 bytes.
211                 box.headerSize += 4;
212             }
213             Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
214         } else if (type == BOX_XYZ && parentType == BOX_ILST) {
215             box.range = new long[] {box.range[0], headerSize,
216                     box.range[0] + headerSize, box.range[1] - headerSize};
217         }
218 
219         if (LOGV) {
220             Log.v(TAG, prefix + "Found box " + typeToString(type)
221                     + " at " + pos + " hdr " + box.headerSize + " length " + len);
222         }
223 
224         // Recursively parse any children boxes
225         if (isBoxParent(type)) {
226             box.children = new ArrayList<>();
227 
228             Box child;
229             while ((child = parseNextBox(fd, pos + len, type, prefix + "  ")) != null) {
230                 box.children.add(child);
231             }
232         }
233 
234         // Skip completely over ourselves
235         Os.lseek(fd, pos + len, OsConstants.SEEK_SET);
236         return box;
237     }
238 
IsoInterface(@onNull FileDescriptor fd)239     private IsoInterface(@NonNull FileDescriptor fd) throws IOException {
240         try {
241             Os.lseek(fd, 4, OsConstants.SEEK_SET);
242             boolean hasFtypHeader;
243             try {
244                 hasFtypHeader = readInt(fd) == BOX_FTYP;
245             } catch (EOFException e) {
246                 hasFtypHeader = false;
247             }
248 
249             if (!hasFtypHeader) {
250                 if (LOGV) {
251                     Log.w(TAG, "Missing 'ftyp' header");
252                 }
253                 return;
254             }
255 
256             final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
257             Os.lseek(fd, 0, OsConstants.SEEK_SET);
258             Box box;
259             while ((box = parseNextBox(fd, end, -1, "")) != null) {
260                 mRoots.add(box);
261             }
262         } catch (ErrnoException e) {
263             throw e.rethrowAsIOException();
264         }
265 
266         // Also create a flattened structure to speed up searching
267         final Queue<Box> queue = new ArrayDeque<>(mRoots);
268         while (!queue.isEmpty()) {
269             final Box box = queue.poll();
270             mFlattened.add(box);
271             if (box.children != null) {
272                 queue.addAll(box.children);
273             }
274         }
275     }
276 
fromFile(@onNull File file)277     public static @NonNull IsoInterface fromFile(@NonNull File file)
278             throws IOException {
279         try (FileInputStream is = new FileInputStream(file)) {
280             return fromFileDescriptor(is.getFD());
281         }
282     }
283 
fromFileDescriptor(@onNull FileDescriptor fd)284     public static @NonNull IsoInterface fromFileDescriptor(@NonNull FileDescriptor fd)
285             throws IOException {
286         return new IsoInterface(fd);
287     }
288 
289     /**
290      * Return a list of content ranges of all boxes of requested type.
291      * <p>
292      * This is always an array of even length, and all values are in exact file
293      * positions (no relative values).
294      */
getBoxRanges(int type)295     public @NonNull long[] getBoxRanges(int type) {
296         LongArray res = new LongArray();
297         for (Box box : mFlattened) {
298             for (int i = 0; i < box.range.length; i += 2) {
299                 if (box.type == type) {
300                     res.add(box.range[i] + box.headerSize);
301                     res.add(box.range[i] + box.range[i + 1]);
302                 }
303             }
304         }
305         return res.toArray();
306     }
307 
getBoxRanges(@onNull UUID uuid)308     public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
309         LongArray res = new LongArray();
310         for (Box box : mFlattened) {
311             for (int i = 0; i < box.range.length; i += 2) {
312                 if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
313                     res.add(box.range[i] + box.headerSize);
314                     res.add(box.range[i] + box.range[i + 1]);
315                 }
316             }
317         }
318         return res.toArray();
319     }
320 
321     /**
322      * Return contents of the first box of requested type.
323      */
getBoxBytes(int type)324     public @Nullable byte[] getBoxBytes(int type) {
325         for (Box box : mFlattened) {
326             if (box.type == type) {
327                 return box.data;
328             }
329         }
330         return null;
331     }
332 
333     /**
334      * Return contents of the first UUID box of requested type.
335      */
getBoxBytes(@onNull UUID uuid)336     public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) {
337         for (Box box : mFlattened) {
338             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
339                 return box.data;
340             }
341         }
342         return null;
343     }
344 
345     /**
346      * Returns whether IsoInterface currently supports parsing data from the specified mime type
347      * or not.
348      *
349      * @param mimeType the string value of mime type
350      */
isSupportedMimeType(@onNull String mimeType)351     public static boolean isSupportedMimeType(@NonNull String mimeType) {
352         if (mimeType == null) {
353             throw new NullPointerException("mimeType shouldn't be null");
354         }
355 
356         switch (mimeType.toLowerCase(Locale.ROOT)) {
357             case "audio/3gp2":
358             case "audio/3gpp":
359             case "audio/3gpp2":
360             case "audio/aac":
361             case "audio/mp4":
362             case "audio/mpeg":
363             case "video/3gp2":
364             case "video/3gpp":
365             case "video/3gpp2":
366             case "video/mj2":
367             case "video/mp4":
368             case "video/mpeg":
369             case "video/x-flv":
370                 return true;
371             default:
372                 return false;
373         }
374     }
375 }
376