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