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             box.data = new byte[(int) (len - box.headerSize)];
181             Os.read(fd, box.data, 0, box.data.length);
182         } else if (type == BOX_XMP) {
183             if (len > Integer.MAX_VALUE) {
184                 Log.w(TAG, "Skipping abnormally large xmp box");
185                 return null;
186             }
187             box.data = new byte[(int) (len - box.headerSize)];
188             Os.read(fd, box.data, 0, box.data.length);
189         } else if (type == BOX_META && len != headerSize) {
190             // The format of this differs in ISO and QT encoding:
191             // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
192             // (qt)  [4 byte size of next atom      ][4 byte hdlr atom type   ]
193             // In case of (iso) we need to skip the next 4 bytes before parsing
194             // the children.
195             readInt(fd);
196             int maybeBoxType = readInt(fd);
197             if (maybeBoxType != BOX_HDLR) {
198                 // ISO, skip 4 bytes.
199                 box.headerSize += 4;
200             }
201             Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
202         }
203 
204         if (LOGV) {
205             Log.v(TAG, prefix + "Found box " + typeToString(type)
206                     + " at " + pos + " hdr " + box.headerSize + " length " + len);
207         }
208 
209         // Recursively parse any children boxes
210         if (isBoxParent(type)) {
211             box.children = new ArrayList<>();
212 
213             Box child;
214             while ((child = parseNextBox(fd, pos + len, prefix + "  ")) != null) {
215                 box.children.add(child);
216             }
217         }
218 
219         // Skip completely over ourselves
220         Os.lseek(fd, pos + len, OsConstants.SEEK_SET);
221         return box;
222     }
223 
IsoInterface(@onNull FileDescriptor fd)224     private IsoInterface(@NonNull FileDescriptor fd) throws IOException {
225         try {
226             Os.lseek(fd, 4, OsConstants.SEEK_SET);
227             boolean hasFtypHeader;
228             try {
229                 hasFtypHeader = readInt(fd) == BOX_FTYP;
230             } catch (EOFException e) {
231                 hasFtypHeader = false;
232             }
233 
234             if (!hasFtypHeader) {
235                 if (LOGV) {
236                     Log.w(TAG, "Missing 'ftyp' header");
237                 }
238                 return;
239             }
240 
241             final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
242             Os.lseek(fd, 0, OsConstants.SEEK_SET);
243             Box box;
244             while ((box = parseNextBox(fd, end, "")) != null) {
245                 mRoots.add(box);
246             }
247         } catch (ErrnoException e) {
248             throw e.rethrowAsIOException();
249         }
250 
251         // Also create a flattened structure to speed up searching
252         final Queue<Box> queue = new ArrayDeque<>(mRoots);
253         while (!queue.isEmpty()) {
254             final Box box = queue.poll();
255             mFlattened.add(box);
256             if (box.children != null) {
257                 queue.addAll(box.children);
258             }
259         }
260     }
261 
fromFile(@onNull File file)262     public static @NonNull IsoInterface fromFile(@NonNull File file)
263             throws IOException {
264         try (FileInputStream is = new FileInputStream(file)) {
265             return fromFileDescriptor(is.getFD());
266         }
267     }
268 
fromFileDescriptor(@onNull FileDescriptor fd)269     public static @NonNull IsoInterface fromFileDescriptor(@NonNull FileDescriptor fd)
270             throws IOException {
271         return new IsoInterface(fd);
272     }
273 
274     /**
275      * Return a list of content ranges of all boxes of requested type.
276      * <p>
277      * This is always an array of even length, and all values are in exact file
278      * positions (no relative values).
279      */
getBoxRanges(int type)280     public @NonNull long[] getBoxRanges(int type) {
281         LongArray res = new LongArray();
282         for (Box box : mFlattened) {
283             if (box.type == type) {
284                 res.add(box.range[0] + box.headerSize);
285                 res.add(box.range[0] + box.range[1]);
286             }
287         }
288         return res.toArray();
289     }
290 
getBoxRanges(@onNull UUID uuid)291     public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
292         LongArray res = new LongArray();
293         for (Box box : mFlattened) {
294             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
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 
302     /**
303      * Return contents of the first box of requested type.
304      */
getBoxBytes(int type)305     public @Nullable byte[] getBoxBytes(int type) {
306         for (Box box : mFlattened) {
307             if (box.type == type) {
308                 return box.data;
309             }
310         }
311         return null;
312     }
313 
314     /**
315      * Return contents of the first UUID box of requested type.
316      */
getBoxBytes(@onNull UUID uuid)317     public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) {
318         for (Box box : mFlattened) {
319             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
320                 return box.data;
321             }
322         }
323         return null;
324     }
325 
326     /**
327      * Returns whether IsoInterface currently supports parsing data from the specified mime type
328      * or not.
329      *
330      * @param mimeType the string value of mime type
331      */
isSupportedMimeType(@onNull String mimeType)332     public static boolean isSupportedMimeType(@NonNull String mimeType) {
333         if (mimeType == null) {
334             throw new NullPointerException("mimeType shouldn't be null");
335         }
336 
337         switch (mimeType.toLowerCase(Locale.ROOT)) {
338             case "audio/3gp2":
339             case "audio/3gpp":
340             case "audio/3gpp2":
341             case "audio/aac":
342             case "audio/mp4":
343             case "audio/mpeg":
344             case "video/3gp2":
345             case "video/3gpp":
346             case "video/3gpp2":
347             case "video/mj2":
348             case "video/mp4":
349             case "video/mpeg":
350             case "video/x-flv":
351                 return true;
352             default:
353                 return false;
354         }
355     }
356 }
357