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