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