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