Mp4SampleEntriesTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.tika.parser.mp4;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

public class Mp4SampleEntriesTest {

    @Test
    public void testClassicEntries() {
        byte[] stsd = stsd(entry(24, "mp4a", 16), entry(24, "ac-3", 16));
        List<String> seen = walk(stsd);
        assertEquals(List.of("mp4a:16:32", "ac-3:40:56"), seen);
    }

    @Test
    public void testLargesizeEntry() {
        //size 1 announces a 64-bit size after the FourCC; the visitor's start
        //must skip the 16 byte header
        byte[] stsd = stsd(largeEntry(32, "avc1", 16), entry(24, "mp4a", 16));
        assertEquals(List.of("avc1:24:40", "mp4a:48:64"), walk(stsd));
    }

    @Test
    public void testZeroSizeEntryExtendsToEnd() {
        byte[] stsd = stsd(entry(0, "hvc1", 16));
        assertEquals(List.of("hvc1:16:32"), walk(stsd));
    }

    @Test
    public void testTruncatedAndUndersizedEntriesStopTheWalk() {
        //an entry claiming more bytes than the payload has
        assertEquals(List.of(), walk(stsd(entry(40, "mp4a", 16))));
        //an entry too small to hold the SampleEntry fields
        assertEquals(List.of(), walk(stsd(entry(12, "mp4a", 4))));
        //a largesize header cut off before the 64-bit size
        byte[] cut = stsd(largeEntry(32, "avc1", 16));
        byte[] truncated = new byte[8 + 12];
        System.arraycopy(cut, 0, truncated, 0, truncated.length);
        assertEquals(List.of(), walk(truncated));
        //a largesize beyond 63 bits
        byte[] huge = stsd(largeEntry(32, "avc1", 16));
        huge[8 + 8] = (byte) 0xFF;
        assertEquals(List.of(), walk(huge));
    }

    @Test
    public void testPrintableFourCC() {
        assertEquals("mp4a", Mp4SampleEntries.printableFourCC(ascii("mp4a"), 0));
        //QuickTime pads short codes with spaces
        assertEquals("raw", Mp4SampleEntries.printableFourCC(ascii("raw "), 0));
        assertEquals("rle", Mp4SampleEntries.printableFourCC(ascii("rle "), 0));
        assertNull(Mp4SampleEntries.printableFourCC(ascii("    "), 0));
        assertNull(Mp4SampleEntries.printableFourCC(new byte[]{0, 1, 2, 3}, 0));
        assertNull(Mp4SampleEntries.printableFourCC(new byte[]{'a', 'v', 'c', 0x7F}, 0));
        assertNull(Mp4SampleEntries.printableFourCC(new byte[]{(byte) 0xE4, 'v', 'c', '1'}, 0));
        //an unprintable FourCC reaches the visitor as null but does not stop the walk
        byte[] stsd = stsd(entry(24, "\u0001vc1", 16), entry(24, "mp4a", 16));
        assertEquals(List.of("null:16:32", "mp4a:40:56"), walk(stsd));
    }

    @Test
    public void testOriginalFormat() {
        byte[] sinf = boxOf("sinf", boxOf("frma", ascii("mp4a")),
                boxOf("schm", new byte[]{0, 0, 0, 0, 'i', 't', 'u', 'n', 0, 1, 0, 0}));
        byte[] children = concat(boxOf("esds", new byte[4]), sinf, boxOf("btrt", new byte[12]));
        assertEquals("mp4a", Mp4SampleEntries.originalFormat(children, 0, children.length));
        //no sinf
        byte[] plain = boxOf("esds", new byte[4]);
        assertNull(Mp4SampleEntries.originalFormat(plain, 0, plain.length));
        //sinf without frma
        byte[] noFrma = boxOf("sinf", boxOf("schi", new byte[0]));
        assertNull(Mp4SampleEntries.originalFormat(noFrma, 0, noFrma.length));
        //frma with an unprintable format
        byte[] bad = boxOf("sinf", boxOf("frma", new byte[]{0, 0, 0, 0}));
        assertNull(Mp4SampleEntries.originalFormat(bad, 0, bad.length));
        //a child box claiming to run past the entry stops the scan
        byte[] truncated = concat(boxOf("esds", new byte[4]), sinf);
        putInt(truncated, 0, 1000);
        assertNull(Mp4SampleEntries.originalFormat(truncated, 0, truncated.length));
        //frma cut off before its payload
        byte[] cut = new byte[16];
        System.arraycopy(sinf, 0, cut, 0, 16);
        putInt(cut, 0, 16);
        assertNull(Mp4SampleEntries.originalFormat(cut, 0, cut.length));
        //a header-only frma (size 8) must not read its format from the next box
        byte[] shortFrma = boxOf("sinf", boxOf("frma"), boxOf("mp4a", new byte[0]));
        assertNull(Mp4SampleEntries.originalFormat(shortFrma, 0, shortFrma.length));
    }

    private static byte[] boxOf(String type, byte[]... payloads) {
        byte[] payload = concat(payloads);
        byte[] b = new byte[8 + payload.length];
        putInt(b, 0, b.length);
        System.arraycopy(ascii(type), 0, b, 4, 4);
        System.arraycopy(payload, 0, b, 8, payload.length);
        return b;
    }

    private static byte[] concat(byte[]... parts) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        for (byte[] part : parts) {
            out.write(part, 0, part.length);
        }
        return out.toByteArray();
    }

    private static List<String> walk(byte[] stsd) {
        List<String> seen = new ArrayList<>();
        Mp4SampleEntries.walk(stsd, (fourCC, b, start, end) ->
                seen.add(fourCC + ":" + start + ":" + end));
        return seen;
    }

    private static byte[] stsd(byte[]... entries) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(new byte[]{0, 0, 0, 0, 0, 0, 0, (byte) entries.length}, 0, 8);
        for (byte[] entry : entries) {
            out.write(entry, 0, entry.length);
        }
        return out.toByteArray();
    }

    private static byte[] entry(int size, String fourCC, int bodyLength) {
        byte[] b = new byte[8 + bodyLength];
        putInt(b, 0, size);
        System.arraycopy(ascii(fourCC), 0, b, 4, 4);
        return b;
    }

    private static byte[] largeEntry(long size, String fourCC, int bodyLength) {
        byte[] b = new byte[16 + bodyLength];
        putInt(b, 0, 1);
        System.arraycopy(ascii(fourCC), 0, b, 4, 4);
        putInt(b, 8, (int) (size >>> 32));
        putInt(b, 12, (int) size);
        return b;
    }

    private static void putInt(byte[] b, int pos, int v) {
        b[pos] = (byte) (v >>> 24);
        b[pos + 1] = (byte) (v >>> 16);
        b[pos + 2] = (byte) (v >>> 8);
        b[pos + 3] = (byte) v;
    }

    private static byte[] ascii(String s) {
        return s.getBytes(StandardCharsets.ISO_8859_1);
    }
}