blob: 35825acd923070e0bc2c7669c5d4d4ac600802cc [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.ExifInterface;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import android.util.LongArray;
import libcore.io.IoBridge;
import libcore.io.Memory;
import java.io.EOFException;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.UUID;
/**
* Simple parser for ISO base media file format. Designed to mirror ergonomics
* of {@link ExifInterface}.
*/
public class IsoInterface {
private static final String TAG = "IsoInterface";
private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
public static final int BOX_FTYP = 0x66747970;
public static final int BOX_UUID = 0x75756964;
public static final int BOX_META = 0x6d657461;
public static final int BOX_XMP = 0x584d505f;
public static final int BOX_LOCI = 0x6c6f6369;
public static final int BOX_XYZ = 0xa978797a;
public static final int BOX_GPS = 0x67707320;
public static final int BOX_GPS0 = 0x67707330;
/**
* Test if given box type is a well-known parent box type.
*/
private static boolean isBoxParent(int type) {
switch (type) {
case 0x6d6f6f76: // moov
case 0x6d6f6f66: // moof
case 0x74726166: // traf
case 0x6d667261: // mfra
case 0x7472616b: // trak
case 0x74726566: // tref
case 0x6d646961: // mdia
case 0x6d696e66: // minf
case 0x64696e66: // dinf
case 0x7374626c: // stbl
case 0x65647473: // edts
case 0x75647461: // udta
case 0x6970726f: // ipro
case 0x73696e66: // sinf
case 0x686e7469: // hnti
case 0x68696e66: // hinf
case 0x6a703268: // jp2h
case 0x696c7374: // ilst
case 0x6d657461: // meta
return true;
default:
return false;
}
}
/** Top-level boxes */
private List<Box> mRoots = new ArrayList<>();
/** Flattened view of all boxes */
private List<Box> mFlattened = new ArrayList<>();
private static class Box {
public final int type;
public final long[] range;
public UUID uuid;
public byte[] data;
public List<Box> children;
public int headerSize;
public Box(int type, long[] range) {
this.type = type;
this.range = range;
}
}
private static String typeToString(int type) {
final byte[] buf = new byte[4];
Memory.pokeInt(buf, 0, type, ByteOrder.BIG_ENDIAN);
return new String(buf);
}
private static int readInt(@NonNull FileDescriptor fd)
throws ErrnoException, IOException {
final byte[] buf = new byte[4];
if (Os.read(fd, buf, 0, 4) == 4) {
return Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
} else {
throw new EOFException();
}
}
private static @NonNull UUID readUuid(@NonNull FileDescriptor fd)
throws ErrnoException, IOException {
final long high = (((long) readInt(fd)) << 32L) | ((long) readInt(fd)) & 0xffffffffL;
final long low = (((long) readInt(fd)) << 32L) | ((long) readInt(fd)) & 0xffffffffL;
return new UUID(high, low);
}
private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
@NonNull String prefix) throws ErrnoException, IOException {
final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
int headerSize = 8;
if (end - pos < headerSize) {
return null;
}
long len = Integer.toUnsignedLong(readInt(fd));
final int type = readInt(fd);
if (len == 0) {
// Length 0 means the box extends to the end of the file.
len = end - pos;
} else if (len == 1) {
// Actually 64-bit box length.
headerSize += 8;
long high = readInt(fd);
long low = readInt(fd);
len = (high << 32L) | (low & 0xffffffffL);
}
if (len < headerSize || pos + len > end) {
Log.w(TAG, "Invalid box at " + pos + " of length " + len
+ ". End of parent " + end);
return null;
}
// Skip past legacy data on 'meta' box
if (type == BOX_META) {
readInt(fd);
}
final Box box = new Box(type, new long[] { pos, len });
box.headerSize = headerSize;
// Parse UUID box
if (type == BOX_UUID) {
box.headerSize += 16;
box.uuid = readUuid(fd);
if (LOGV) {
Log.v(TAG, prefix + " UUID " + box.uuid);
}
if (len > Integer.MAX_VALUE) {
Log.w(TAG, "Skipping abnormally large uuid box");
return null;
}
box.data = new byte[(int) (len - box.headerSize)];
IoBridge.read(fd, box.data, 0, box.data.length);
}
// Parse XMP box
if (type == BOX_XMP) {
if (len > Integer.MAX_VALUE) {
Log.w(TAG, "Skipping abnormally large xmp box");
return null;
}
box.data = new byte[(int) (len - box.headerSize)];
IoBridge.read(fd, box.data, 0, box.data.length);
}
if (LOGV) {
Log.v(TAG, prefix + "Found box " + typeToString(type)
+ " at " + pos + " hdr " + box.headerSize + " length " + len);
}
// Recursively parse any children boxes
if (isBoxParent(type)) {
box.children = new ArrayList<>();
Box child;
while ((child = parseNextBox(fd, pos + len, prefix + " ")) != null) {
box.children.add(child);
}
}
// Skip completely over ourselves
Os.lseek(fd, pos + len, OsConstants.SEEK_SET);
return box;
}
private IsoInterface(@NonNull FileDescriptor fd) throws IOException {
try {
Os.lseek(fd, 4, OsConstants.SEEK_SET);
if (readInt(fd) != BOX_FTYP) {
if (LOGV) {
Log.w(TAG, "Missing 'ftyp' header");
}
return;
}
final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
Os.lseek(fd, 0, OsConstants.SEEK_SET);
Box box;
while ((box = parseNextBox(fd, end, "")) != null) {
mRoots.add(box);
}
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
// Also create a flattened structure to speed up searching
final Queue<Box> queue = new LinkedList<>(mRoots);
while (!queue.isEmpty()) {
final Box box = queue.poll();
mFlattened.add(box);
if (box.children != null) {
queue.addAll(box.children);
}
}
}
public static @NonNull IsoInterface fromFile(@NonNull File file)
throws IOException {
try (FileInputStream is = new FileInputStream(file)) {
return fromFileDescriptor(is.getFD());
}
}
public static @NonNull IsoInterface fromFileDescriptor(@NonNull FileDescriptor fd)
throws IOException {
return new IsoInterface(fd);
}
/**
* Return a list of content ranges of all boxes of requested type.
* <p>
* This is always an array of even length, and all values are in exact file
* positions (no relative values).
*/
public @NonNull long[] getBoxRanges(int type) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
if (box.type == type) {
res.add(box.range[0] + box.headerSize);
res.add(box.range[0] + box.range[1]);
}
}
return res.toArray();
}
public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
res.add(box.range[0] + box.headerSize);
res.add(box.range[0] + box.range[1]);
}
}
return res.toArray();
}
/**
* Return contents of the first box of requested type.
*/
public @Nullable byte[] getBoxBytes(int type) {
for (Box box : mFlattened) {
if (box.type == type) {
return box.data;
}
}
return null;
}
/**
* Return contents of the first UUID box of requested type.
*/
public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) {
for (Box box : mFlattened) {
if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
return box.data;
}
}
return null;
}
}