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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.media.ExifInterface;
22 import android.system.ErrnoException;
23 import android.system.Os;
24 import android.system.OsConstants;
25 import android.util.Log;
26 import android.util.LongArray;
27 
28 import libcore.io.IoBridge;
29 import libcore.io.Memory;
30 
31 import java.io.EOFException;
32 import java.io.File;
33 import java.io.FileDescriptor;
34 import java.io.FileInputStream;
35 import java.io.IOException;
36 import java.nio.ByteOrder;
37 import java.util.ArrayList;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Objects;
41 import java.util.Queue;
42 import java.util.UUID;
43 
44 /**
45  * Simple parser for ISO base media file format. Designed to mirror ergonomics
46  * of {@link ExifInterface}.
47  */
48 public class IsoInterface {
49     private static final String TAG = "IsoInterface";
50     private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
51 
52     public static final int BOX_FTYP = 0x66747970;
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 
Box(int type, long[] range)104         public Box(int type, long[] range) {
105             this.type = type;
106             this.range = range;
107         }
108     }
109 
typeToString(int type)110     private static String typeToString(int type) {
111         final byte[] buf = new byte[4];
112         Memory.pokeInt(buf, 0, type, ByteOrder.BIG_ENDIAN);
113         return new String(buf);
114     }
115 
readInt(@onNull FileDescriptor fd)116     private static int readInt(@NonNull FileDescriptor fd)
117             throws ErrnoException, IOException {
118         final byte[] buf = new byte[4];
119         if (Os.read(fd, buf, 0, 4) == 4) {
120             return Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
121         } else {
122             throw new EOFException();
123         }
124     }
125 
readUuid(@onNull FileDescriptor fd)126     private static @NonNull UUID readUuid(@NonNull FileDescriptor fd)
127             throws ErrnoException, IOException {
128         final long high = (((long) readInt(fd)) << 32L) | ((long) readInt(fd)) & 0xffffffffL;
129         final long low = (((long) readInt(fd)) << 32L) | ((long) readInt(fd)) & 0xffffffffL;
130         return new UUID(high, low);
131     }
132 
parseNextBox(@onNull FileDescriptor fd, long end, @NonNull String prefix)133     private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
134             @NonNull String prefix) throws ErrnoException, IOException {
135         final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
136         if (pos == end) {
137             return null;
138         }
139 
140         final long len = Integer.toUnsignedLong(readInt(fd));
141         if (len <= 0 || pos + len > end) {
142             Log.w(TAG, "Invalid box at " + pos + " of length " + len
143                     + " reached beyond end of parent " + end);
144             return null;
145         }
146 
147         // Skip past legacy data on 'meta' box
148         final int type = readInt(fd);
149         if (type == BOX_META) {
150             readInt(fd);
151         }
152 
153         final Box box = new Box(type, new long[] { pos, len });
154         if (LOGV) {
155             Log.v(TAG, prefix + "Found box " + typeToString(type)
156                     + " at " + pos + " length " + len);
157         }
158 
159         // Parse UUID box
160         if (type == BOX_UUID) {
161             box.uuid = readUuid(fd);
162             if (LOGV) {
163                 Log.v(TAG, prefix + "  UUID " + box.uuid);
164             }
165 
166             box.data = new byte[(int) (len - 8 - 16)];
167             IoBridge.read(fd, box.data, 0, box.data.length);
168         }
169 
170         // Parse XMP box
171         if (type == BOX_XMP) {
172             box.data = new byte[(int) (len - 8)];
173             IoBridge.read(fd, box.data, 0, box.data.length);
174         }
175 
176         // Recursively parse any children boxes
177         if (isBoxParent(type)) {
178             box.children = new ArrayList<>();
179 
180             Box child;
181             while ((child = parseNextBox(fd, pos + len, prefix + "  ")) != null) {
182                 box.children.add(child);
183             }
184         }
185 
186         // Skip completely over ourselves
187         Os.lseek(fd, pos + len, OsConstants.SEEK_SET);
188         return box;
189     }
190 
IsoInterface(@onNull FileDescriptor fd)191     private IsoInterface(@NonNull FileDescriptor fd) throws IOException {
192         try {
193             Os.lseek(fd, 4, OsConstants.SEEK_SET);
194             if (readInt(fd) != BOX_FTYP) {
195                 if (LOGV) {
196                     Log.w(TAG, "Missing 'ftyp' header");
197                 }
198                 return;
199             }
200 
201             final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
202             Os.lseek(fd, 0, OsConstants.SEEK_SET);
203             Box box;
204             while ((box = parseNextBox(fd, end, "")) != null) {
205                 mRoots.add(box);
206             }
207         } catch (ErrnoException e) {
208             throw e.rethrowAsIOException();
209         }
210 
211         // Also create a flattened structure to speed up searching
212         final Queue<Box> queue = new LinkedList<>(mRoots);
213         while (!queue.isEmpty()) {
214             final Box box = queue.poll();
215             mFlattened.add(box);
216             if (box.children != null) {
217                 queue.addAll(box.children);
218             }
219         }
220     }
221 
fromFile(@onNull File file)222     public static @NonNull IsoInterface fromFile(@NonNull File file)
223             throws IOException {
224         try (FileInputStream is = new FileInputStream(file)) {
225             return fromFileDescriptor(is.getFD());
226         }
227     }
228 
fromFileDescriptor(@onNull FileDescriptor fd)229     public static @NonNull IsoInterface fromFileDescriptor(@NonNull FileDescriptor fd)
230             throws IOException {
231         return new IsoInterface(fd);
232     }
233 
234     /**
235      * Return a list of content ranges of all boxes of requested type.
236      * <p>
237      * This is always an array of even length, and all values are in exact file
238      * positions (no relative values).
239      */
getBoxRanges(int type)240     public @NonNull long[] getBoxRanges(int type) {
241         LongArray res = new LongArray();
242         for (Box box : mFlattened) {
243             if (box.type == type) {
244                 res.add(box.range[0] + 8);
245                 res.add(box.range[0] + box.range[1]);
246             }
247         }
248         return res.toArray();
249     }
250 
getBoxRanges(@onNull UUID uuid)251     public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
252         LongArray res = new LongArray();
253         for (Box box : mFlattened) {
254             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
255                 res.add(box.range[0] + 8 + 16);
256                 res.add(box.range[0] + box.range[1]);
257             }
258         }
259         return res.toArray();
260     }
261 
262     /**
263      * Return contents of the first box of requested type.
264      */
getBoxBytes(int type)265     public @Nullable byte[] getBoxBytes(int type) {
266         for (Box box : mFlattened) {
267             if (box.type == type) {
268                 return box.data;
269             }
270         }
271         return null;
272     }
273 
274     /**
275      * Return contents of the first UUID box of requested type.
276      */
getBoxBytes(@onNull UUID uuid)277     public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) {
278         for (Box box : mFlattened) {
279             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
280                 return box.data;
281             }
282         }
283         return null;
284     }
285 }
286