Split Get() into Identity and Features

Usual clients such as php-src begin by identifying the bitstream's
format and only after start to parse it for width, height etc.
Splitting the function in two avoids a stream rewind and/or parsing
the same data twice.

Change-Id: I4fc814e442bebe6f48d94f634c7e86dd07c73867
diff --git a/avifinfo.c b/avifinfo.c
index 88a6e11..5b93d19 100644
--- a/avifinfo.c
+++ b/avifinfo.c
@@ -24,6 +24,13 @@
   kInvalid,  // Input incorrectly parsed.
 } AvifInfoInternalStatus;
 
+static AvifInfoStatus AvifInfoInternalConvertStatus(AvifInfoInternalStatus s) {
+  return (s == kFound)                         ? kAvifInfoOk
+         : (s == kNotFound || s == kTruncated) ? kAvifInfoNotEnoughData
+         : (s == kAborted)                     ? kAvifInfoTooComplex
+                                               : kAvifInfoInvalidFile;
+}
+
 // uint32_t is used everywhere in this file. It is unlikely to be insufficient
 // to parse AVIF headers.
 #define AVIFINFO_MAX_SIZE UINT32_MAX
@@ -611,8 +618,30 @@
 
 //------------------------------------------------------------------------------
 
-// Parses a file 'stream'. The file type is checked through the "ftyp" box and
-// 'features' are extracted through the "meta" box.
+// Parses a file 'stream'. The file type is checked through the "ftyp" box.
+static AvifInfoInternalStatus ParseFtyp(AvifInfoInternalStream* stream) {
+  AvifInfoInternalBox box;
+  uint32_t num_parsed_boxes = 0;
+  AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
+                                                &num_parsed_boxes, &box));
+  AVIFINFO_CHECK(!memcmp(box.type, "ftyp", 4), kInvalid);
+  // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
+  AVIFINFO_CHECK(box.content_size >= 8, kInvalid);  // major_brand,minor_version
+  for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) {
+    const uint8_t* data;
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
+    if (i == 4) continue;  // Skip minor_version.
+    if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) {
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalSkip(stream, box.content_size - (i + 4)));
+      return kFound;
+    }
+    AVIFINFO_CHECK(i <= 32 * 4, kAborted);  // Be reasonable.
+  }
+  AVIFINFO_RETURN(kInvalid);  // No AVIF brand no good.
+}
+
+// Parses a file 'stream'. 'features' are extracted from the "meta" box.
 static AvifInfoInternalStatus ParseFile(AvifInfoInternalStream* stream,
                                         uint32_t* num_parsed_boxes,
                                         AvifInfoInternalFeatures* features) {
@@ -620,28 +649,7 @@
     AvifInfoInternalBox box;
     AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
                                                   num_parsed_boxes, &box));
-    // The first box must be "ftyp" and no other box at root can be "ftyp".
-    AVIFINFO_CHECK((*num_parsed_boxes == 1) == !memcmp(box.type, "ftyp", 4),
-                   kInvalid);
-
-    if (!memcmp(box.type, "ftyp", 4)) {
-      // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
-      AVIFINFO_CHECK(box.content_size >= 8, kInvalid);  // major_brand + version
-      uint8_t is_avif = 0;
-      for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) {
-        const uint8_t* data;
-        AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
-        if (i == 4) continue;  // Skip minor_version.
-        if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) {
-          is_avif = 1;
-          AVIFINFO_CHECK_FOUND(
-              AvifInfoInternalSkip(stream, box.content_size - (i + 4)));
-          break;
-        }
-        AVIFINFO_CHECK(i <= 32 * 4, kAborted);  // Be reasonable.
-      }
-      AVIFINFO_CHECK(is_avif, kInvalid);
-    } else if (!memcmp(box.type, "meta", 4)) {
+    if (!memcmp(box.type, "meta", 4)) {
       return ParseMeta(stream, box.content_size, num_parsed_boxes, features);
     } else {
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
@@ -678,22 +686,43 @@
 //------------------------------------------------------------------------------
 // Fixed-size input public API
 
-AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size,
-                           AvifInfoFeatures* features) {
+AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size) {
   AvifInfoInternalForward stream;
   stream.data = data;
   stream.data_size = data_size;
   // Forward null 'data' as a null 'stream' to handle it the same way.
-  return AvifInfoRead((void*)&stream,
-                      (data == NULL) ? NULL : AvifInfoInternalForwardRead,
-                      AvifInfoInternalForwardSkip, features);
+  return AvifInfoIdentifyStream(
+      (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead,
+      AvifInfoInternalForwardSkip);
+}
+
+AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size,
+                                   AvifInfoFeatures* features) {
+  AvifInfoInternalForward stream;
+  stream.data = data;
+  stream.data_size = data_size;
+  return AvifInfoGetFeaturesStream(
+      (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead,
+      AvifInfoInternalForwardSkip, features);
 }
 
 //------------------------------------------------------------------------------
 // Streamed input API
 
-AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
-                            skip_stream_t skip, AvifInfoFeatures* features) {
+AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read,
+                                    skip_stream_t skip) {
+  if (read == NULL) return kAvifInfoNotEnoughData;
+
+  AvifInfoInternalStream internal_stream;
+  internal_stream.stream = stream;
+  internal_stream.read = read;
+  internal_stream.skip = skip;  // Fallbacks to 'read' if null.
+  return AvifInfoInternalConvertStatus(ParseFtyp(&internal_stream));
+}
+
+AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read,
+                                    skip_stream_t skip,
+                                    AvifInfoFeatures* features) {
   if (features != NULL) memset(features, 0, sizeof(*features));
   if (read == NULL) return kAvifInfoNotEnoughData;
 
@@ -708,14 +737,9 @@
   // Go through all relevant boxes sequentially.
   const AvifInfoInternalStatus status =
       ParseFile(&internal_stream, &num_parsed_boxes, &internal_features);
-
-  if (status == kNotFound) return kAvifInfoNotEnoughData;
-  if (status == kTruncated) return kAvifInfoNotEnoughData;
-  if (status == kInvalid) return kAvifInfoInvalidFile;
-  if (status == kAborted) return kAvifInfoTooComplex;
-  if (features != NULL) {
+  if (status == kFound && features != NULL) {
     memcpy(features, &internal_features.primary_item_features,
            sizeof(*features));
   }
-  return kAvifInfoOk;
+  return AvifInfoInternalConvertStatus(status);
 }
diff --git a/avifinfo.h b/avifinfo.h
index a940ea7..4c898c2 100644
--- a/avifinfo.h
+++ b/avifinfo.h
@@ -45,12 +45,16 @@
 // Fixed-size input API
 // Use this API if a raw byte array of fixed size is available as input.
 
-// Parses the AVIF 'data' and extracts its 'features'.
+// Parses the 'data' and returns kAvifInfoOk if it is identified as an AVIF.
+// The file type can be identified in the first 12 bytes of most AVIF files.
+AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size);
+
+// Parses the identified AVIF 'data' and extracts its 'features'.
 // 'data' can be partial but must point to the beginning of the AVIF file.
 // The 'features' can be parsed in the first 450 bytes of most AVIF files.
 // 'features' are set to 0 unless kAvifInfoOk is returned.
-AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size,
-                           AvifInfoFeatures* features);
+AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size,
+                                   AvifInfoFeatures* features);
 
 //------------------------------------------------------------------------------
 // Streamed input API
@@ -69,11 +73,15 @@
 // Maximum number of bytes requested per read. There is no limit per skip.
 #define AVIFINFO_MAX_NUM_READ_BYTES 64
 
-// Same as AvifInfoGet() but takes a 'stream' as input. AvifInfoRead() does not
-// access the 'stream' directly but passes it as is to 'read' and 'skip'.
+// Same as AvifInfo*() but takes a 'stream' as input. AvifInfo*Stream() does
+// not access the 'stream' directly but passes it as is to 'read' and 'skip'.
 // 'read' cannot be null. If 'skip' is null, 'read' is called instead.
-AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
-                            skip_stream_t skip, AvifInfoFeatures* features);
+AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read,
+                                      skip_stream_t skip);
+// Can be called right after AvifInfoIdentifyStream() with the same 'stream'.
+AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read,
+                                         skip_stream_t skip,
+                                         AvifInfoFeatures* features);
 
 //------------------------------------------------------------------------------
 
diff --git a/tests/avifinfo_fuzz.cc b/tests/avifinfo_fuzz.cc
index d4a6526..be39869 100644
--- a/tests/avifinfo_fuzz.cc
+++ b/tests/avifinfo_fuzz.cc
@@ -47,7 +47,8 @@
 
 // Test a random bitstream of random size, whether it is valid or not.
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t data_size) {
-  AvifInfoStatus previous_status = kAvifInfoNotEnoughData;
+  AvifInfoStatus previous_status_identity = kAvifInfoNotEnoughData;
+  AvifInfoStatus previous_status_features = kAvifInfoNotEnoughData;
   AvifInfoFeatures previous_features = {0};
 
   // Check the consistency of the returned status and features:
@@ -56,22 +57,26 @@
   for (size_t size = 0; size < data_size; ++size) {
     StreamData stream = {data, size};
     AvifInfoFeatures features;
-    const AvifInfoStatus status =
-        AvifInfoRead(&stream, StreamRead, StreamSkip, &features);
+    const AvifInfoStatus status_identity =
+        AvifInfoIdentifyStream(&stream, StreamRead, StreamSkip);
+    const AvifInfoStatus status_features =
+        AvifInfoGetFeaturesStream(&stream, StreamRead, StreamSkip, &features);
 
-    if (previous_status != kAvifInfoNotEnoughData &&
-        status != previous_status) {
+    if ((previous_status_identity != kAvifInfoNotEnoughData &&
+         status_identity != previous_status_identity) ||
+        (previous_status_features != kAvifInfoNotEnoughData &&
+         status_features != previous_status_features)) {
       std::abort();
     }
 
-    if (status == previous_status) {
+    if (status_features == previous_status_features) {
       if (features.width != previous_features.width ||
           features.height != previous_features.height ||
           features.bit_depth != previous_features.bit_depth ||
           features.num_channels != previous_features.num_channels) {
         std::abort();
       }
-    } else if (status == kAvifInfoOk) {
+    } else if (status_features == kAvifInfoOk) {
       if (features.width == 0u || features.height == 0u ||
           features.bit_depth == 0u || features.num_channels == 0u) {
         std::abort();
@@ -83,7 +88,8 @@
       }
     }
 
-    previous_status = status;
+    previous_status_identity = status_identity;
+    previous_status_features = status_features;
     previous_features = features;
   }
   return 0;
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc
index 0ab2ac7..c0cb7c7 100644
--- a/tests/avifinfo_test.cc
+++ b/tests/avifinfo_test.cc
@@ -41,9 +41,10 @@
   const Data input = LoadFile("avifinfo_test_1x1.avif");
   ASSERT_FALSE(input.empty());
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
 }
 
 TEST(AvifInfoGetTest, NoPixi10b) {
@@ -54,9 +55,10 @@
       LoadFile("avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif");
   ASSERT_FALSE(input.empty());
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(features, {1u, 1u, 10u, 3u}));
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  EXPECT_TRUE(AreEqual(f, {1u, 1u, 10u, 3u}));
 }
 
 TEST(AvifInfoGetTest, EnoughBytes) {
@@ -67,39 +69,28 @@
   input.resize(std::search(input.begin(), input.end(), kMdatTag, kMdatTag + 4) -
                input.begin());
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
-}
-
-TEST(AvifInfoGetTest, BigMetaBox) {
-  Data input = LoadFile("avifinfo_test_1x1.avif");
-  ASSERT_FALSE(input.empty());
-  // Change "meta" box size to the size 2^32-1.
-  const uint8_t kMetaTag[] = {'m', 'e', 't', 'a'};
-  auto meta_tag =
-      std::search(input.begin(), input.end(), kMetaTag, kMetaTag + 4);
-  meta_tag[-4] = meta_tag[-3] = meta_tag[-2] = meta_tag[-1] = 255;
-
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
 }
 
 TEST(AvifInfoGetTest, Null) {
   const Data input = LoadFile("avifinfo_test_1x1.avif");
   ASSERT_FALSE(input.empty());
 
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), nullptr), kAvifInfoOk);
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr),
+            kAvifInfoOk);
 }
 
 //------------------------------------------------------------------------------
 // Negative tests
 
 TEST(AvifInfoGetTest, Empty) {
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(nullptr, 0, &features), kAvifInfoNotEnoughData);
-  EXPECT_TRUE(AreEqual(features, {0}));
+  EXPECT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData);
+  EXPECT_TRUE(AreEqual(f, {0}));
 }
 
 TEST(AvifInfoGetTest, NotEnoughBytes) {
@@ -110,8 +101,9 @@
   input.resize(std::search(input.begin(), input.end(), kIpmaTag, kIpmaTag + 4) -
                input.begin());
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoNotEnoughData);
 }
 
@@ -122,10 +114,11 @@
   const uint8_t kIspeTag[] = {'i', 's', 'p', 'e'};
   std::search(input.begin(), input.end(), kIspeTag, kIspeTag + 4)[0] = 'a';
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoInvalidFile);
-  EXPECT_TRUE(AreEqual(features, {0}));
+  EXPECT_TRUE(AreEqual(f, {0}));
 }
 
 TEST(AvifInfoGetTest, MetaBoxIsTooBig) {
@@ -139,10 +132,11 @@
   meta_tag[-1] = 1;  // 32-bit "1" then 4-char "meta" then 64-bit size.
   input.insert(meta_tag + 4, {255, 255, 255, 255, 255, 255, 255, 255});
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoTooComplex);
-  EXPECT_TRUE(AreEqual(features, {0}));
+  EXPECT_TRUE(AreEqual(f, {0}));
 }
 
 TEST(AvifInfoGetTest, TooManyBoxes) {
@@ -156,18 +150,22 @@
     input.insert(input.end(), kBox, kBox + kBox[3]);
   }
 
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoGet(reinterpret_cast<uint8_t*>(input.data()),
-                        input.size() * 4, &features),
+  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()),
+                                input.size() * 4, &f),
             kAvifInfoTooComplex);
 }
 
 TEST(AvifInfoReadTest, Null) {
-  AvifInfoFeatures features;
-  EXPECT_EQ(AvifInfoRead(/*stream=*/nullptr, /*read=*/nullptr, /*skip=*/nullptr,
-                         &features),
+  EXPECT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr,
+                                   /*skip=*/nullptr),
             kAvifInfoNotEnoughData);
-  EXPECT_TRUE(AreEqual(features, {0}));
+  AvifInfoFeatures f;
+  EXPECT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr,
+                                      /*skip=*/nullptr, &f),
+            kAvifInfoNotEnoughData);
+  EXPECT_TRUE(AreEqual(f, {0}));
 }
 
 //------------------------------------------------------------------------------
diff --git a/tools/avifinfo_tool.cc b/tools/avifinfo_tool.cc
index 0d0dc13..4d4e4ae 100644
--- a/tools/avifinfo_tool.cc
+++ b/tools/avifinfo_tool.cc
@@ -70,8 +70,9 @@
 // Parses the AVIF at 'data' of 'data_size' bytes using libavifinfo.
 Result ParseAvif(const uint8_t data[], size_t data_size) {
   Result result;
-  const AvifInfoStatus status = AvifInfoGet(data, data_size, &result.features);
-  result.success = (status == kAvifInfoOk);
+  result.success =
+      (AvifInfoIdentify(data, data_size) == kAvifInfoOk &&
+       AvifInfoGetFeatures(data, data_size, &result.features) == kAvifInfoOk);
   return result;
 }
 
@@ -88,7 +89,7 @@
   size_t max_data_size = data_size;
   while (min_data_size < max_data_size) {
     const size_t middle = (min_data_size + max_data_size) / 2;
-    if (AvifInfoGet(data, middle, nullptr) == kAvifInfoOk) {
+    if (ParseAvif(data, middle).success) {
       max_data_size = middle;
     } else {
       min_data_size = middle + 1;