/*
 * Copyright (C) 2014 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.google.android.exoplayer.extractor.mp4;

import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;

import java.io.IOException;

/**
 * Provides methods that peek data from an {@link ExtractorInput} and return whether the input
 * appears to be in MP4 format.
 */
/* package */ final class Sniffer {

  private static final int[] COMPATIBLE_BRANDS = new int[] {
      Util.getIntegerCodeForString("isom"),
      Util.getIntegerCodeForString("iso2"),
      Util.getIntegerCodeForString("avc1"),
      Util.getIntegerCodeForString("hvc1"),
      Util.getIntegerCodeForString("hev1"),
      Util.getIntegerCodeForString("mp41"),
      Util.getIntegerCodeForString("mp42"),
      Util.getIntegerCodeForString("3g2a"),
      Util.getIntegerCodeForString("3g2b"),
      Util.getIntegerCodeForString("3gr6"),
      Util.getIntegerCodeForString("3gs6"),
      Util.getIntegerCodeForString("3ge6"),
      Util.getIntegerCodeForString("3gg6"),
      Util.getIntegerCodeForString("M4V "),
      Util.getIntegerCodeForString("M4A "),
      Util.getIntegerCodeForString("f4v "),
      Util.getIntegerCodeForString("kddi"),
      Util.getIntegerCodeForString("M4VP"),
      Util.getIntegerCodeForString("qt  "), // Apple QuickTime
      Util.getIntegerCodeForString("MSNV"), // Sony PSP
  };

  /**
   * Returns whether data peeked from the current position in {@code input} is consistent with the
   * input being a fragmented MP4 file.
   *
   * @param input The extractor input from which to peek data. The peek position will be modified.
   * @return True if the input appears to be in the fragmented MP4 format. False otherwise.
   * @throws IOException If an error occurs reading from the input.
   * @throws InterruptedException If the thread has been interrupted.
   */
  public static boolean sniffFragmented(ExtractorInput input)
      throws IOException, InterruptedException {
    return sniffInternal(input, 4 * 1024, true);
  }

  /**
   * Returns whether data peeked from the current position in {@code input} is consistent with the
   * input being an unfragmented MP4 file.
   *
   * @param input The extractor input from which to peek data. The peek position will be modified.
   * @return True if the input appears to be in the unfragmented MP4 format. False otherwise.
   * @throws IOException If an error occurs reading from the input.
   * @throws InterruptedException If the thread has been interrupted.
   */
  public static boolean sniffUnfragmented(ExtractorInput input)
      throws IOException, InterruptedException {
    return sniffInternal(input, 128, false);
  }

  private static boolean sniffInternal(ExtractorInput input, int searchLength, boolean fragmented)
      throws IOException, InterruptedException {
    long inputLength = input.getLength();
    int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > searchLength
        ? searchLength : inputLength);

    ParsableByteArray buffer = new ParsableByteArray(64);
    int bytesSearched = 0;
    boolean foundGoodFileType = false;
    boolean foundFragment = false;
    while (bytesSearched < bytesToSearch) {
      // Read an atom header.
      int headerSize = Atom.HEADER_SIZE;
      input.peekFully(buffer.data, 0, headerSize);
      buffer.setPosition(0);
      long atomSize = buffer.readUnsignedInt();
      int atomType = buffer.readInt();
      if (atomSize == Atom.LONG_SIZE_PREFIX) {
        input.peekFully(buffer.data, headerSize, Atom.LONG_HEADER_SIZE - headerSize);
        headerSize = Atom.LONG_HEADER_SIZE;
        atomSize = buffer.readLong();
      }
      // Check the atom size is large enough to include its header.
      if (atomSize < headerSize || atomSize > Integer.MAX_VALUE) {
        return false;
      }
      // Stop searching if reading this atom would exceed the search limit.
      if (bytesSearched + atomSize > bytesToSearch) {
        break;
      }
      int atomDataSize = (int) atomSize - headerSize;
      if (atomType == Atom.TYPE_ftyp) {
        if (atomDataSize < 8) {
          return false;
        }
        int compatibleBrandsCount = (atomDataSize - 8) / 4;
        input.peekFully(buffer.data, 0, 4 * (compatibleBrandsCount + 2));
        for (int i = 0; i < compatibleBrandsCount + 2; i++) {
          if (i == 1) {
            // This index refers to the minorVersion, not a brand, so skip it.
            continue;
          }
          if (isCompatibleBrand(buffer.readInt())) {
            foundGoodFileType = true;
            break;
          }
        }
      } else if (atomType == Atom.TYPE_moof) {
        foundFragment = true;
        break;
      } else if (atomDataSize != 0) {
        input.advancePeekPosition(atomDataSize);
      }
      bytesSearched += atomSize;
    }
    return foundGoodFileType && fragmented == foundFragment;
  }

  /**
   * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors.
   */
  private static boolean isCompatibleBrand(int brand) {
    // Accept all brands starting '3gp'.
    if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) {
      return true;
    }
    for (int compatibleBrand : COMPATIBLE_BRANDS) {
      if (compatibleBrand == brand) {
        return true;
      }
    }
    return false;
  }

  private Sniffer() {
    // Prevent instantiation.
  }

}

@@ -4449,217 +4449,32 @@
 Size
- %7C%7C atomSize %3E Integer.MAX_VALUE) %7B%0A        return false;%0A      %7D%0A      // Stop searching if reading this atom would exceed the search limit.%0A      if (bytesSearched + atomSize %3E bytesToSearch) %7B%0A        break
+) %7B%0A        return false
 ;%0A  
@@ -5114,131 +5114,461 @@
     
-%7D else if (atomType == Atom.TYPE_moof) %7B%0A        foundFragment = true;%0A        break;%0A      %7D else if (atomDataSize != 0) %7B
+  // There is only one ftyp box, so reject the file if the file type in this box was invalid.%0A        if (!foundGoodFileType) %7B%0A          return false;%0A        %7D%0A      %7D else if (atomType == Atom.TYPE_moof) %7B%0A        foundFragment = true;%0A        break;%0A      %7D else if (atomDataSize != 0) %7B%0A        // Stop searching if reading this atom would exceed the search limit.%0A        if (bytesSearched + atomSize %3E= bytesToSearch) %7B%0A          break;%0A        %7D
 %0A   

Improve sniffer behavior for fragmented MP4.