Support multiple extents in an ItemLocationBox

Fixes: #291
diff --git a/src/read.c b/src/read.c
index 97246ce..40bdb1b 100644
--- a/src/read.c
+++ b/src/read.c
@@ -118,21 +118,29 @@
     return NULL;
 }
 
+typedef struct avifDecoderItemExtent
+{
+    uint32_t offset;
+    uint32_t size;
+} avifDecoderItemExtent;
+AVIF_ARRAY_DECLARE(avifDecoderItemExtentArray, avifDecoderItemExtent, extent);
+
 // one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these)
 typedef struct avifDecoderItem
 {
     uint32_t id;
     struct avifMeta * meta; // Unowned; A back-pointer for convenience
     uint8_t type[4];
-    uint32_t offset;
     uint32_t size;
     uint32_t idatID; // If non-zero, offset is relative to this idat box (iloc construction_method==1)
     avifContentType contentType;
     avifPropertyArray properties;
-    uint32_t thumbnailForID; // if non-zero, this item is a thumbnail for Item #{thumbnailForID}
-    uint32_t auxForID;       // if non-zero, this item is an auxC plane for Item #{auxForID}
-    uint32_t descForID;      // if non-zero, this item is a content description for Item #{descForID}
-    uint32_t dimgForID;      // if non-zero, this item is a derived image for Item #{dimgForID}
+    avifDecoderItemExtentArray extents; // All extent offsets/sizes
+    avifRWData mergedExtents; // if populated, is a single contiguous block of this item's extents (unused when extents.count == 1)
+    uint32_t thumbnailForID;  // if non-zero, this item is a thumbnail for Item #{thumbnailForID}
+    uint32_t auxForID;        // if non-zero, this item is an auxC plane for Item #{auxForID}
+    uint32_t descForID;       // if non-zero, this item is a content description for Item #{descForID}
+    uint32_t dimgForID;       // if non-zero, this item is a derived image for Item #{dimgForID}
     avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so.
 } avifDecoderItem;
 AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem, item);
@@ -453,6 +461,8 @@
     for (uint32_t i = 0; i < meta->items.count; ++i) {
         avifDecoderItem * item = &meta->items.item[i];
         avifArrayDestroy(&item->properties);
+        avifArrayDestroy(&item->extents);
+        avifRWDataFree(&item->mergedExtents);
     }
     avifArrayDestroy(&meta->items);
     avifArrayDestroy(&meta->properties);
@@ -474,6 +484,7 @@
 
     avifDecoderItem * item = (avifDecoderItem *)avifArrayPushPtr(&meta->items);
     avifArrayCreate(&item->properties, sizeof(avifProperty), 16);
+    avifArrayCreate(&item->extents, sizeof(avifDecoderItemExtent), 1);
     item->id = itemID;
     item->meta = meta;
     return item;
@@ -581,6 +592,12 @@
 
 static const uint8_t * avifDecoderDataCalcItemPtr(avifDecoderData * data, avifDecoderItem * item)
 {
+    if (item->mergedExtents.data) {
+        // Multiple extents have already been concatenated for this item, just return it
+        return item->mergedExtents.data;
+    }
+
+    // Find this item's source of all extents' data, based on the construction method
     avifROData * offsetBuffer = NULL;
     if (item->idatID == 0) {
         // construction_method: file(0)
@@ -603,14 +620,48 @@
         }
     }
 
-    if (item->offset > offsetBuffer->size) {
+    if (item->extents.count == 0) {
+        return NULL;
+    } else if (item->extents.count == 1) {
+        // A single extent; just use the pre-existing buffer
+
+        avifDecoderItemExtent * extent = &item->extents.extent[0];
+        if (extent->offset > offsetBuffer->size) {
+            return NULL;
+        }
+        uint64_t offsetSize = (uint64_t)extent->offset + (uint64_t)extent->size;
+        if (offsetSize > (uint64_t)offsetBuffer->size) {
+            return NULL;
+        }
+        return offsetBuffer->data + extent->offset;
+    }
+
+    // Multiple extents; merge them into a single contiguous buffer
+    avifRWDataRealloc(&item->mergedExtents, item->size);
+    uint8_t * front = item->mergedExtents.data;
+    size_t remainingBytes = item->mergedExtents.size;
+    for (uint32_t extentIter = 0; extentIter < item->extents.count; ++extentIter) {
+        avifDecoderItemExtent * extent = &item->extents.extent[extentIter];
+        if (extent->offset > offsetBuffer->size) {
+            return NULL;
+        }
+        uint64_t offsetSize = (uint64_t)extent->offset + (uint64_t)extent->size;
+        if (offsetSize > (uint64_t)offsetBuffer->size) {
+            return NULL;
+        }
+        if ((size_t)extent->size > remainingBytes) {
+            return NULL;
+        }
+
+        memcpy(front, offsetBuffer->data + extent->offset, extent->size);
+        front += extent->size;
+        remainingBytes -= extent->size;
+    }
+    if (remainingBytes != 0) {
+        // This should be impossible?
         return NULL;
     }
-    uint64_t offsetSize = (uint64_t)item->offset + (uint64_t)item->size;
-    if (offsetSize > (uint64_t)offsetBuffer->size) {
-        return NULL;
-    }
-    return offsetBuffer->data + item->offset;
+    return item->mergedExtents.data;
 }
 
 static avifBool avifDecoderDataGenerateImageGridTiles(avifDecoderData * data, avifImageGrid * grid, avifDecoderItem * gridItem, avifBool alpha)
@@ -949,13 +1000,20 @@
             }
         }
 
+        avifDecoderItem * item = avifMetaFindItem(meta, itemID);
+        if (!item) {
+            return AVIF_FALSE;
+        }
+        item->id = itemID;
+        item->idatID = idatID;
+
         uint16_t dataReferenceIndex;                                 // unsigned int(16) data_ref rence_index;
         CHECK(avifROStreamReadU16(&s, &dataReferenceIndex));         //
         uint64_t baseOffset;                                         // unsigned int(base_offset_size*8) base_offset;
         CHECK(avifROStreamReadUX8(&s, &baseOffset, baseOffsetSize)); //
         uint16_t extentCount;                                        // unsigned int(16) extent_count;
         CHECK(avifROStreamReadU16(&s, &extentCount));                //
-        if (extentCount == 1) {
+        for (int extentIter = 0; extentIter < extentCount; ++extentIter) {
             // If extent_index is ever supported, this spec must be implemented here:
             // ::  if (((version == 1) || (version == 2)) && (index_size > 0)) {
             // ::      unsigned int(index_size*8) extent_index;
@@ -966,17 +1024,10 @@
             uint64_t extentLength; // unsigned int(offset_size*8) extent_length;
             CHECK(avifROStreamReadUX8(&s, &extentLength, lengthSize));
 
-            avifDecoderItem * item = avifMetaFindItem(meta, itemID);
-            if (!item) {
-                return AVIF_FALSE;
-            }
-            item->id = itemID;
-            item->offset = (uint32_t)(baseOffset + extentOffset);
-            item->size = (uint32_t)extentLength;
-            item->idatID = idatID;
-        } else {
-            // TODO: support more than one extent
-            return AVIF_FALSE;
+            avifDecoderItemExtent * extent = (avifDecoderItemExtent *)avifArrayPushPtr(&item->extents);
+            extent->offset = (uint32_t)(baseOffset + extentOffset);
+            extent->size = (uint32_t)extentLength;
+            item->size += extent->size;
         }
     }
     return AVIF_TRUE;