Commit 0cc60d75 for libheif

commit 0cc60d759e33a30f6b3240967a02c4ef5a37301b
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Aug 27 02:53:12 2026 +0200

    harden the generic item writers

    heif_context_add_item / add_mime_item / add_precompressed_mime_item /
    add_uri_item:

    - NULL content_type / content_encoding / item_uri_type were passed straight into
      std::string and threw std::logic_error through the C API (SIGABRT). A negative
      size was converted to size_t (std::length_error, abort), NULL data with
      size > 0 was dereferenced. All of these now return heif_error_Usage_error, and
      the bodies run under exception_guard.
    - The copy-pasted success/error tail (five copies, which is why the Result bug was
      fixed piecemeal) is a single helper. A NULL out_item_id is allowed: the item is
      added, its ID is not reported. Doxygen comments added for all four functions.
    - HeifFile::add_infe (behind heif_context_add_item) used add_new_infe_box, which
      runs init_for_image(): the file got a 'pict' handler, an empty iprp and a 'pitm'
      with item ID 0, so the written file was unreadable ("pitm references
      non-existing image") and a sequence-only file lost its readability. It now uses
      add_new_meta_infe_box like the mime/uri writers. The property writers create
      the iprp/ipco/ipma boxes on demand (init_for_item_properties()) so properties
      can still be attached to such items.
    - Errors from set_item_data / set_precompressed_item_data were discarded: with an
      unavailable compressor (e.g. a build without zlib/brotli) the call reported
      success and a valid ID for an item without any iloc data. The error is now
      propagated and the half-created infe box is removed again. Requesting a
      compression for a non-mime item is an error instead of a TODO, and empty data
      no longer memcpy()s from a NULL pointer.
    - heif_item_get_item_data: do not write *out_data_size on error when it is NULL.

diff --git a/libheif/api/libheif/heif_items.cc b/libheif/api/libheif/heif_items.cc
index f4e9357d..af92e95e 100644
--- a/libheif/api/libheif/heif_items.cc
+++ b/libheif/api/libheif/heif_items.cc
@@ -150,9 +150,11 @@ heif_error heif_item_get_item_data(const heif_context* ctx,

   auto dataResult = ctx->context->get_heif_file()->get_item_data(item_id, out_compression_format);
   if (!dataResult) {
-    *out_data_size = 0;
+    if (out_data_size) {
+      *out_data_size = 0;
+    }
     if (out_data) {
-      *out_data = 0;
+      *out_data = nullptr;
     }

     return dataResult.error_struct(ctx->context.get());
@@ -301,6 +303,42 @@ heif_error heif_context_add_item_references(heif_context* ctx,

 // ------------------------- writing -------------------------

+// Check the (data, size) pair that all item writers take. 'size' is a public 'int' that
+// is converted to size_t further down: a negative value would turn into a huge allocation
+// (std::length_error through the C API), and a NULL buffer with a non-zero size would be
+// dereferenced.
+static heif_error check_item_data_arguments(const void* data, int size)
+{
+  if (size < 0) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "item data size must not be negative"};
+  }
+
+  if (size > 0 && data == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  return heif_error_success;
+}
+
+
+// Common tail of all item writers: report the error, or store the new item ID.
+// A NULL out_item_id is allowed (the item is added, but its ID is not reported).
+static heif_error return_new_item_id(heif_context* ctx, Result<heif_item_id> result, heif_item_id* out_item_id)
+{
+  if (!result) {
+    return result.error_struct(ctx->context.get());
+  }
+
+  if (out_item_id) {
+    *out_item_id = *result;
+  }
+
+  return heif_error_success;
+}
+
+
 heif_error heif_context_add_item(heif_context* ctx,
                                  const char* item_type,
                                  const void* data, int size,
@@ -314,15 +352,14 @@ heif_error heif_context_add_item(heif_context* ctx,
     };
   }

-  Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe(fourcc(item_type), (const uint8_t*) data, size);
-
-  if (result && out_item_id) {
-    *out_item_id = *result;
-    return heif_error_success;
-  }
-  else {
-    return result.error_struct(ctx->context.get());
+  if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) {
+    return err;
   }
+
+  return exception_guard([&]() -> heif_error {
+    Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe(fourcc(item_type), (const uint8_t*) data, size);
+    return return_new_item_id(ctx, std::move(result), out_item_id);
+  });
 }

 heif_error heif_context_add_mime_item(heif_context* ctx,
@@ -331,15 +368,18 @@ heif_error heif_context_add_mime_item(heif_context* ctx,
                                       const void* data, int size,
                                       heif_item_id* out_item_id)
 {
-  Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe_mime(content_type, content_encoding, (const uint8_t*) data, size);
-
-  if (result && out_item_id) {
-    *out_item_id = *result;
-    return heif_error_success;
+  if (content_type == nullptr) {
+    return heif_error_null_pointer_argument;
   }
-  else {
-    return result.error_struct(ctx->context.get());
+
+  if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) {
+    return err;
   }
+
+  return exception_guard([&]() -> heif_error {
+    Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe_mime(content_type, content_encoding, (const uint8_t*) data, size);
+    return return_new_item_id(ctx, std::move(result), out_item_id);
+  });
 }


@@ -349,15 +389,18 @@ heif_error heif_context_add_precompressed_mime_item(heif_context* ctx,
                                                     const void* data, int size,
                                                     heif_item_id* out_item_id)
 {
-  Result<heif_item_id> result = ctx->context->get_heif_file()->add_precompressed_infe_mime(content_type, content_encoding, (const uint8_t*) data, size);
-
-  if (result && out_item_id) {
-    *out_item_id = *result;
-    return heif_error_success;
+  if (content_type == nullptr || content_encoding == nullptr) {
+    return heif_error_null_pointer_argument;
   }
-  else {
-    return result.error_struct(ctx->context.get());
+
+  if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) {
+    return err;
   }
+
+  return exception_guard([&]() -> heif_error {
+    Result<heif_item_id> result = ctx->context->get_heif_file()->add_precompressed_infe_mime(content_type, content_encoding, (const uint8_t*) data, size);
+    return return_new_item_id(ctx, std::move(result), out_item_id);
+  });
 }

 heif_error heif_context_add_uri_item(heif_context* ctx,
@@ -365,13 +408,16 @@ heif_error heif_context_add_uri_item(heif_context* ctx,
                                      const void* data, int size,
                                      heif_item_id* out_item_id)
 {
-  Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe_uri(item_uri_type, (const uint8_t*) data, size);
-
-  if (result && out_item_id) {
-    *out_item_id = *result;
-    return heif_error_success;
+  if (item_uri_type == nullptr) {
+    return heif_error_null_pointer_argument;
   }
-  else {
-    return result.error_struct(ctx->context.get());
+
+  if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) {
+    return err;
   }
+
+  return exception_guard([&]() -> heif_error {
+    Result<heif_item_id> result = ctx->context->get_heif_file()->add_infe_uri(item_uri_type, (const uint8_t*) data, size);
+    return return_new_item_id(ctx, std::move(result), out_item_id);
+  });
 }
diff --git a/libheif/api/libheif/heif_items.h b/libheif/api/libheif/heif_items.h
index b446b702..46bf71e8 100644
--- a/libheif/api/libheif/heif_items.h
+++ b/libheif/api/libheif/heif_items.h
@@ -227,12 +227,50 @@ heif_error heif_context_add_item_references(heif_context* ctx,

 // ------------------------- adding new items -------------------------

+/*
+ * All functions in this section add a new item to the context and return its ID in
+ * 'out_item_id'. The item ID is freshly allocated and does not collide with any item that
+ * already exists in the context, including items read from an input file.
+ *
+ * 'data' is copied into the context, the caller may release it afterwards. 'size' is the
+ * number of bytes in 'data'. A NULL 'data' is only allowed together with size 0.
+ * 'out_item_id' may be NULL, in which case the item is added but its ID is not reported.
+ *
+ * The new item is marked as hidden. Use heif_context_add_item_reference() to link it to an
+ * image (e.g. with a 'cdsc' reference for metadata that describes the image).
+ *
+ * Note that a file must contain at least one image or image sequence to be written;
+ * a context holding only items added with these functions cannot be written.
+ */
+
+/**
+ * Add an item with an arbitrary four-character item type (e.g. "Exif").
+ *
+ * @param ctx the file context
+ * @param item_type four-character item type
+ * @param data the item payload
+ * @param size number of bytes in 'data'
+ * @param out_item_id receives the ID of the new item (may be NULL)
+ */
 LIBHEIF_API
 heif_error heif_context_add_item(heif_context* ctx,
                                  const char* item_type,
                                  const void* data, int size,
                                  heif_item_id* out_item_id);

+/**
+ * Add a 'mime' item, optionally compressing the payload.
+ *
+ * @param ctx the file context
+ * @param content_type MIME content type of the payload (e.g. "application/rdf+xml")
+ * @param content_encoding compression to apply to the payload before storing it.
+ *        Use heif_metadata_compression_method_supported() to check which methods this
+ *        build of libheif can compress. An unsupported method returns an error and no
+ *        item is added.
+ * @param data the (uncompressed) item payload
+ * @param size number of bytes in 'data'
+ * @param out_item_id receives the ID of the new item (may be NULL)
+ */
 LIBHEIF_API
 heif_error heif_context_add_mime_item(heif_context* ctx,
                                       const char* content_type,
@@ -240,6 +278,21 @@ heif_error heif_context_add_mime_item(heif_context* ctx,
                                       const void* data, int size,
                                       heif_item_id* out_item_id);

+/**
+ * Add a 'mime' item whose payload has already been compressed by the caller.
+ *
+ * @param ctx the file context
+ * @param content_type MIME content type of the (uncompressed) payload
+ * @param content_encoding the HTTP content-coding name that was applied to 'data', stored
+ *        verbatim as the item's content_encoding. Use "" for uncompressed data.
+ *        libheif itself can decode "deflate", "compress_zlib" and "br" (when built with
+ *        zlib / Brotli support) and treats "identity" as uncompressed; items with any other
+ *        content_encoding are kept, but their data can only be retrieved in compressed form
+ *        with heif_item_get_item_data().
+ * @param data the compressed item payload
+ * @param size number of bytes in 'data'
+ * @param out_item_id receives the ID of the new item (may be NULL)
+ */
 LIBHEIF_API
 heif_error heif_context_add_precompressed_mime_item(heif_context* ctx,
                                                     const char* content_type,
@@ -247,6 +300,15 @@ heif_error heif_context_add_precompressed_mime_item(heif_context* ctx,
                                                     const void* data, int size,
                                                     heif_item_id* out_item_id);

+/**
+ * Add a 'uri ' item.
+ *
+ * @param ctx the file context
+ * @param item_uri_type the URI that identifies the type of the item payload
+ * @param data the item payload
+ * @param size number of bytes in 'data'
+ * @param out_item_id receives the ID of the new item (may be NULL)
+ */
 LIBHEIF_API
 heif_error heif_context_add_uri_item(heif_context* ctx,
                                      const char* item_uri_type,
diff --git a/libheif/file.cc b/libheif/file.cc
index d1f7fdd0..d9050f30 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -212,6 +212,14 @@ void HeifFile::init_for_image()
     m_meta_box->append_child_box(m_pitm_box);
   }

+  init_for_item_properties();
+}
+
+
+void HeifFile::init_for_item_properties()
+{
+  init_for_meta_item();
+
   if (!m_iprp_box) {
     m_iprp_box = std::make_shared<Box_iprp>();
     m_meta_box->append_child_box(m_iprp_box);
@@ -1068,6 +1076,16 @@ void HeifFile::seed_id_creator()
 }


+void HeifFile::remove_infe_box(const std::shared_ptr<Box_infe>& infe)
+{
+  m_infe_boxes.erase(infe->get_item_ID());
+
+  if (m_iinf_box) {
+    m_iinf_box->remove_child_box(infe);
+  }
+}
+
+
 Result<heif_item_id> HeifFile::add_new_image(uint32_t item_type)
 {
   auto result = add_new_infe_box(item_type);
@@ -1124,6 +1142,8 @@ Result<std::shared_ptr<Box_infe>> HeifFile::add_new_meta_infe_box(uint32_t item_

 void HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential)
 {
+  init_for_item_properties();
+
   auto ispe = std::make_shared<Box_ispe>();
   ispe->set_size(width, height);

@@ -1136,6 +1156,8 @@ void HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t heigh

 heif_property_id HeifFile::add_property(heif_item_id id, const std::shared_ptr<Box>& property, bool essential)
 {
+  init_for_item_properties();
+
   uint32_t index = m_ipco_box->find_or_append_child_box(property);

   m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
@@ -1146,6 +1168,8 @@ heif_property_id HeifFile::add_property(heif_item_id id, const std::shared_ptr<B

 heif_property_id HeifFile::add_property_without_deduplication(heif_item_id id, const std::shared_ptr<Box>& property, bool essential)
 {
+  init_for_item_properties();
+
   uint32_t index = m_ipco_box->append_child_box(property);

   m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)});
@@ -1156,6 +1180,8 @@ heif_property_id HeifFile::add_property_without_deduplication(heif_item_id id, c

 void HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orientation)
 {
+  init_for_item_properties();
+
   // Note: ISO/IEC 23000-22:2019(E) (MIAF) 7.3.6.7 requires the following order:
   // clean aperture first, then rotation, then mirror

@@ -1220,7 +1246,7 @@ Result<heif_item_id> HeifFile::add_infe(uint32_t item_type, const uint8_t* data,
 {
   // create an infe box describing what kind of data we are storing (this also creates a new ID)

-  auto infe_result = add_new_infe_box(item_type);
+  auto infe_result = add_new_meta_infe_box(item_type);
   if (!infe_result) {
     return infe_result.error();
   }
@@ -1229,7 +1255,10 @@ Result<heif_item_id> HeifFile::add_infe(uint32_t item_type, const uint8_t* data,

   heif_item_id metadata_id = infe_box->get_item_ID();

-  set_item_data(infe_box, data, size, heif_metadata_compression_off);
+  if (Error err = set_item_data(infe_box, data, size, heif_metadata_compression_off)) {
+    remove_infe_box(infe_box);
+    return err;
+  }

   return metadata_id;
 }
@@ -1255,7 +1284,10 @@ Result<heif_item_id> HeifFile::add_infe_mime(const char* content_type, heif_meta

   heif_item_id metadata_id = infe_box->get_item_ID();

-  set_item_data(infe_box, data, size, content_encoding);
+  if (Error err = set_item_data(infe_box, data, size, content_encoding)) {
+    remove_infe_box(infe_box);
+    return err;
+  }

   return metadata_id;
 }
@@ -1275,7 +1307,10 @@ Result<heif_item_id> HeifFile::add_precompressed_infe_mime(const char* content_t

   heif_item_id metadata_id = infe_box->get_item_ID();

-  set_precompressed_item_data(infe_box, data, size, content_encoding);
+  if (Error err = set_precompressed_item_data(infe_box, data, size, content_encoding)) {
+    remove_infe_box(infe_box);
+    return err;
+  }

   return metadata_id;
 }
@@ -1295,7 +1330,10 @@ Result<heif_item_id> HeifFile::add_infe_uri(const char* item_uri_type, const uin

   heif_item_id metadata_id = infe_box->get_item_ID();

-  set_item_data(infe_box, data, size, heif_metadata_compression_off);
+  if (Error err = set_item_data(infe_box, data, size, heif_metadata_compression_off)) {
+    remove_infe_box(infe_box);
+    return err;
+  }

   return metadata_id;
 }
@@ -1312,7 +1350,9 @@ Error HeifFile::set_item_data(const std::shared_ptr<Box_infe>& item, const uint8
   // only set metadata compression for MIME type data which has 'content_encoding' field
   if (compression != heif_metadata_compression_off &&
       item->get_item_type_4cc() != fourcc("mime")) {
-    // TODO: error, compression not supported
+    return Error(heif_error_Usage_error,
+                 heif_suberror_Invalid_parameter_value,
+                 "Item data compression is only supported for 'mime' items");
   }


@@ -1366,13 +1406,17 @@ Error HeifFile::set_precompressed_item_data(const std::shared_ptr<Box_infe>& ite
   // only set metadata compression for MIME type data which has 'content_encoding' field
   if (!content_encoding.empty() &&
       item->get_item_type_4cc() != fourcc("mime")) {
-    // TODO: error, compression not supported
+    return Error(heif_error_Usage_error,
+                 heif_suberror_Invalid_parameter_value,
+                 "A content_encoding can only be set for 'mime' items");
   }


   std::vector<uint8_t> data_array;
-  data_array.resize(size);
-  memcpy(data_array.data(), data, size);
+  if (size > 0) { // do not memcpy() from a NULL pointer when there is no data
+    data_array.resize(size);
+    memcpy(data_array.data(), data, size);
+  }

   item->set_content_encoding(content_encoding);

@@ -1487,6 +1531,8 @@ std::shared_ptr<Box_EntityToGroup> HeifFile::get_entity_group(heif_entity_group_

 void HeifFile::set_auxC_property(heif_item_id id, const std::string& type)
 {
+  init_for_item_properties();
+
   auto auxC = std::make_shared<Box_auxC>();
   auxC->set_aux_type(type);

diff --git a/libheif/file.h b/libheif/file.h
index fe4388cd..7691342f 100644
--- a/libheif/file.h
+++ b/libheif/file.h
@@ -88,6 +88,10 @@ public:

   void init_for_image();

+  // Create the 'iprp'/'ipco'/'ipma' boxes if they do not exist yet. Unlike init_for_image(),
+  // this does not turn the file into an image file (no 'pict' handler, no 'pitm').
+  void init_for_item_properties();
+
   void init_for_sequence();

   void set_hdlr_box(std::shared_ptr<Box_hdlr> box) { m_hdlr_box = std::move(box); }
@@ -308,6 +312,9 @@ private:
   // file, so that IDs allocated for new items cannot collide with existing ones.
   void seed_id_creator();

+  // Undo add_new_infe_box() / add_new_meta_infe_box() when the item could not be completed.
+  void remove_infe_box(const std::shared_ptr<Box_infe>& infe);
+
   Error parse_heif_images();

   Error parse_heif_sequences();