Merge "Minor fixes and small refactor in libavifinfo" into main
diff --git a/README.md b/README.md
index 83d9a6f..7d17d55 100644
--- a/README.md
+++ b/README.md
@@ -6,69 +6,44 @@
 See `avifinfo.h` for details on the API and `avifinfo.c` for the implementation.
 See `tests/avifinfo_demo.cc` for API usage examples.
 
-## Contents
-
-1.  [How to use](#how-to-use)
-
-    1.  [Build](#build)
-    2.  [Test](#test)
-
-2.  [Development](#development)
-
-    1.  [Coding style](#coding-style)
-    2.  [Submitting patches](#submitting-patches)
-
-3.  [PHP implementation](#php-implementation)
-
-4.  [Bug reports](#bug-reports)
-
-## How to use {#how-to-use}
+## How to use
 
 **libavifinfo** can be used when only a few AVIF features are needed and when
 linking to or including [libavif](https://github.com/AOMediaCodec/libavif) is
 not an option. For decoding an image or extracting more features, please rely on
 [libavif](https://github.com/AOMediaCodec/libavif).
 
-Note: `AvifInfoGetFeatures()` is designed to return the same `avifImage` field
-values as
+Note: The C implementation `AvifInfoGetFeatures()` is designed to return the
+same `avifImage` field values as
 [`avifDecoderParse()`](https://github.com/AOMediaCodec/libavif/blob/e34204f5370509c72b3b2f065e5ebb2767cbbd48/include/avif/avif.h#L1049).
-However **libavifinfo** is more permissive and may return features of images
-considered invalid by **libavif**.
+However **libavifinfo** is often more permissive and may return features of
+images considered invalid by **libavif**.
 
-### Build {#build}
+## C implementation
 
-`avifinfo.c` is written in C. To build from this directory:
-
-```sh
-mkdir build && \
-cd build && \
-cmake .. && \
-cmake --build . --config Release
-```
-
-### Test {#test}
-
-Tests are written in C++. GoogleTest is required.
-
-```sh
-mkdir build && \
-cd build && \
-cmake .. -DAVIFINFO_BUILD_TESTS=ON && \
-cmake --build . --config Debug && \
-ctest .
-```
-
-## Development {#development}
-
-### Coding style {#coding-style}
+### Coding style
 
 [Google C/C++ Style Guide](https://google.github.io/styleguide/cppguide.html) is
 used in this project.
 
-### Submitting patches {#submitting-patches}
+### Build
 
-If you would like to contribute to **libavifinfo**, please follow the steps for
-**libaom** at https://aomedia.googlesource.com/aom/#submitting-patches.
+`avifinfo.c` is written in C. To build from this directory:
+
+```sh
+cmake -S . -B build && \
+cmake --build build --config Release
+```
+
+### Test
+
+GoogleTest is required for the C++ tests (e.g. `sudo apt install libgtest-dev`).
+
+```sh
+cmake -S . -B build -DAVIFINFO_BUILD_TESTS=ON && \
+cmake --build build --config Debug && \
+ctest --test-dir build build
+```
 
 ## PHP implementation
 
@@ -81,7 +56,40 @@
 
 See `avifinfo_test.php` for a usage example.
 
-## Bug reports {#bug-reports}
+### PHP test
+
+```sh
+cd tests && \
+php avifinfo_test.php
+```
+
+## Development
+
+### Submitting patches {#submitting-patches}
+
+If you would like to contribute to **libavifinfo**, please follow the steps for
+**libaom** at https://aomedia.googlesource.com/aom/#submitting-patches. Summary:
+
+- Log in at https://aomedia.googlesource.com
+- Sign in at https://aomedia-review.googlesource.com
+- Execute the contributor agreement if necessary, see http://aomedia.org/license
+- Test all implementations as described in this document
+- Copy the commit message hook:
+  `cd libavifinfo && curl -Lo .git/hooks/commit-msg https://chromium-review.googlesource.com/tools/hooks/commit-msg && chmod u+x .git/hooks/commit-msg`
+- Create a commit
+- `git push https://aomedia.googlesource.com/libavifinfo/ HEAD:refs/for/main`
+- Request a review from a project maintainer
+
+## Known users of libavifinfo
+
+- [Chromium](https://source.chromium.org/chromium/chromium/src/+/main:third_party/libavifinfo)
+  uses the C impl to extract the gain map item for HDR tone mapping.
+- [PHP-src](https://github.com/php/php-src/tree/master/ext/standard/libavifinfo)
+  uses the C impl to parse image features without relying on libavif or libheif.
+- [WordPress](https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-avif-info.php)
+  uses the PHP impl to support AVIF images with older versions of PHP.
+
+## Bug reports
 
 Bug reports can be filed in the Alliance for Open Media
 [issue tracker](https://bugs.chromium.org/p/aomedia/issues/list).
diff --git a/avifinfo.c b/avifinfo.c
index 62abf28..599c019 100644
--- a/avifinfo.c
+++ b/avifinfo.c
@@ -318,11 +318,18 @@
     // Read the 32 least-significant bits.
     box->size = AvifInfoInternalReadBigEndian(data + 4, sizeof(uint32_t));
   } else if (box->size == 0) {
+    // ISO/IEC 14496-12 4.2.2:
+    //   if size is 0, then this box shall be in a top-level box
+    //   (i.e. not contained in another box)
+    AVIFINFO_CHECK(nesting_level == 0, kInvalid);
     box->size = num_remaining_bytes;
   }
   AVIFINFO_CHECK(box->size >= box_header_size, kInvalid);
   AVIFINFO_CHECK(box->size <= num_remaining_bytes, kInvalid);
 
+  // 16 bytes of usertype should be read here if the box type is 'uuid'.
+  // 'uuid' boxes are skipped so usertype is part of the skipped body.
+
   const int has_fullbox_header =
       !memcmp(box->type, "meta", 4) || !memcmp(box->type, "pitm", 4) ||
       !memcmp(box->type, "ipma", 4) || !memcmp(box->type, "ispe", 4) ||
@@ -483,7 +490,11 @@
         if (strcmp(aux_type, kGainmapStr) == 0) {
           // Note: It is unlikely but it is possible that this gain map
           // does not belong to the primary item or a tile. Ignore this issue.
-          features->gainmap_property_index = box_index;
+          if (box_index <= AVIFINFO_MAX_VALUE) {
+            features->gainmap_property_index = (uint8_t)box_index;
+          } else {
+            features->data_was_skipped = 1;
+          }
         } else if (box.content_size >= kAlphaStrLength &&
                    memcmp(aux_type, kAlphaStr, kGainmapStrLength) == 0) {
           // The beginning of the aux type matches the alpha aux type string.
@@ -511,7 +522,7 @@
   AVIFINFO_RETURN(kNotFound);
 }
 
-// Parses a 'stream' of an "iprp" box into 'features'. The "ipco" box contain
+// Parses a 'stream' of an "iprp" box into 'features'. The "ipco" box contains
 // the properties which are linked to items by the "ipma" box.
 static AvifInfoInternalStatus ParseIprp(int nesting_level,
                                         AvifInfoInternalStream* stream,
@@ -692,38 +703,37 @@
 
     if (!memcmp(box.type, "infe", 4)) {
       // See ISO/IEC 14496-12:2015(E) 8.11.6.2
-      uint32_t num_read_bytes = 0;
-
       const uint32_t num_bytes_per_id = (box.version == 2) ? 2 : 4;
       const uint8_t* data;
       // item_ID (16 or 32) + item_protection_index (16) + item_type (32).
-      AVIFINFO_CHECK((num_bytes_per_id + 6) <= num_remaining_bytes, kInvalid);
+      AVIFINFO_CHECK(num_bytes_per_id + 2 + 4 <= box.content_size, kInvalid);
       AVIFINFO_CHECK_FOUND(
           AvifInfoInternalRead(stream, num_bytes_per_id, &data));
-      num_read_bytes += num_bytes_per_id;
       const uint32_t item_id =
           AvifInfoInternalReadBigEndian(data, num_bytes_per_id);
 
-      // Skip item_protection_index
+      // Skip item_protection_index.
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, 2));
-      num_read_bytes += 2;
 
       const uint8_t* item_type;
       AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &item_type));
-      num_read_bytes += 4;
       if (!memcmp(item_type, "tmap", 4)) {
         // Tone Mapped Image: indicates the presence of a gain map.
-        features->tone_mapped_item_id = item_id;
+        if (item_id <= AVIFINFO_MAX_VALUE) {
+          features->tone_mapped_item_id = (uint8_t)item_id;
+        } else {
+          features->data_was_skipped = 1;
+        }
       }
 
-      AVIFINFO_CHECK_FOUND(
-          AvifInfoInternalSkip(stream, box.content_size - num_read_bytes));
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(
+          stream, box.content_size - (num_bytes_per_id + 2 + 4)));
     } else {
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
 
     num_remaining_bytes -= box.size;
-    if (num_remaining_bytes <= 0) break;
+    if (num_remaining_bytes == 0) break;  // Ignore entry_count bigger than box.
   }
   AVIFINFO_RETURN(kNotFound);
 }
@@ -822,7 +832,6 @@
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
   }
-  AVIFINFO_RETURN(kInvalid);  // No "meta" no good.
 }
 
 //------------------------------------------------------------------------------
diff --git a/avifinfo.php b/avifinfo.php
index bebcc35..81269ec 100644
--- a/avifinfo.php
+++ b/avifinfo.php
@@ -250,6 +250,10 @@
       // Read the 32 least-significant bits.
       $this->size = read_big_endian( substr( $data, 4, 4 ), 4 );
     } else if ( $this->size == 0 ) {
+      // ISO/IEC 14496-12 4.2.2:
+      //   if size is 0, then this box shall be in a top-level box
+      //   (i.e. not contained in another box)
+      // Unfortunately the presence of a parent box is unknown here.
       $this->size = $num_remaining_bytes;
     }
     if ( $this->size < $header_size ) {
@@ -259,6 +263,9 @@
       return INVALID;
     }
 
+    // 16 bytes of usertype should be read here if the box type is 'uuid'.
+    // 'uuid' boxes are skipped so usertype is part of the skipped body.
+
     $has_fullbox_header = $this->type == 'meta' || $this->type == 'pitm' ||
                           $this->type == 'ipma' || $this->type == 'ispe' ||
                           $this->type == 'pixi' || $this->type == 'iref' ||
@@ -477,7 +484,7 @@
   /**
    * Parses an "iprp" box.
    *
-   * The "ipco" box contain the properties which are linked to items by the "ipma" box.
+   * The "ipco" box contains the properties which are linked to items by the "ipma" box.
    *
    * @param stream  $handle              The resource the box will be parsed from.
    * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.