Fix support for 0-length hex-encoded octet strings in chip-tool. (#24097)

* Fix support for 0-length hex-encoded octet strings in chip-tool.

Without this fix, these commands all fail to parse:

  chip-tool unittesting write struct-attr '{"a": 1, "b": true, "c": 0, "d":"", "e": "abc", "f": 0, "g": 0, "h": 0}' 17 1
  chip-tool unittesting write struct-attr '{"a": 1, "b": true, "c": 0, "d":"hex:", "e": "abc", "f": 0, "g": 0, "h": 0}' 17 1
  chip-tool unittesting write octet-string "hex:" 17 1
  chip-tool unittesting write-by-id 0 "hex:" 17 1

and with this fix they parse and send correct payloads.

Fixes https://github.com/project-chip/connectedhomeip/issues/24054

* Apply suggestion from review comment.

Co-authored-by: Jonathan Mégevand <77852424+jmeg-sfy@users.noreply.github.com>

* Address review comment.

Co-authored-by: Jonathan Mégevand <77852424+jmeg-sfy@users.noreply.github.com>
diff --git a/examples/chip-tool/BUILD.gn b/examples/chip-tool/BUILD.gn
index d870c1d..86e003a 100644
--- a/examples/chip-tool/BUILD.gn
+++ b/examples/chip-tool/BUILD.gn
@@ -58,6 +58,7 @@
     "commands/common/Commands.cpp",
     "commands/common/Commands.h",
     "commands/common/CredentialIssuerCommands.h",
+    "commands/common/HexConversion.h",
     "commands/discover/DiscoverCommand.cpp",
     "commands/discover/DiscoverCommissionablesCommand.cpp",
     "commands/discover/DiscoverCommissionersCommand.cpp",
diff --git a/examples/chip-tool/commands/clusters/ComplexArgument.h b/examples/chip-tool/commands/clusters/ComplexArgument.h
index 2af80e5..e624aeb 100644
--- a/examples/chip-tool/commands/clusters/ComplexArgument.h
+++ b/examples/chip-tool/commands/clusters/ComplexArgument.h
@@ -36,6 +36,7 @@
 #include <app-common/zap-generated/cluster-objects.h>
 #include <app/data-model/List.h>
 #include <app/data-model/Nullable.h>
+#include <commands/common/HexConversion.h>
 #include <json/json.h>
 #include <lib/core/Optional.h>
 #include <lib/support/BytesToHex.h>
@@ -210,18 +211,22 @@
                 size = str.size();
             }
 
-            if (size % 2 != 0)
-            {
-                ChipLogError(chipTool, "Error while encoding %s as a hex string: Odd number of characters.", label);
-                return CHIP_ERROR_INVALID_STRING_LENGTH;
-            }
+            CHIP_ERROR err = HexToBytes(
+                chip::CharSpan(str.c_str(), size),
+                [&buffer](size_t allocSize) {
+                    buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(allocSize, sizeof(uint8_t)));
+                    return buffer;
+                },
+                &size);
 
-            buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(size / 2, sizeof(uint8_t)));
-            size   = chip::Encoding::HexToBytes(str.c_str(), size, buffer, size / 2);
-            if (size == 0)
+            if (err != CHIP_NO_ERROR)
             {
-                ChipLogError(chipTool, "Error while encoding %s as a hex string.", label);
-                return CHIP_ERROR_INTERNAL;
+                if (buffer != nullptr)
+                {
+                    chip::Platform::MemoryFree(buffer);
+                }
+
+                return err;
             }
         }
 
diff --git a/examples/chip-tool/commands/clusters/CustomArgument.h b/examples/chip-tool/commands/clusters/CustomArgument.h
index 63fe7d7..ab6ce40 100644
--- a/examples/chip-tool/commands/clusters/CustomArgument.h
+++ b/examples/chip-tool/commands/clusters/CustomArgument.h
@@ -19,6 +19,7 @@
 #pragma once
 
 #include <app-common/zap-generated/cluster-objects.h>
+#include <commands/common/HexConversion.h>
 #include <lib/support/BytesToHex.h>
 #include <lib/support/CHIPMemString.h>
 #include <lib/support/SafeInt.h>
@@ -140,14 +141,18 @@
 
     static CHIP_ERROR PutOctetString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
     {
-        size_t size = strlen(value.asCString());
-        VerifyOrReturnError(size % 2 == 0, CHIP_ERROR_INVALID_STRING_LENGTH);
-
+        const char * hexData = value.asCString() + kPayloadHexPrefixLen;
+        size_t hexDataLen    = strlen(hexData);
         chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
-        VerifyOrReturnError(buffer.Calloc(size / 2), CHIP_ERROR_NO_MEMORY);
-        size_t octetCount = chip::Encoding::HexToBytes(value.asCString() + kPayloadHexPrefixLen, size - kPayloadHexPrefixLen,
-                                                       buffer.Get(), (size - kPayloadHexPrefixLen) / 2);
-        VerifyOrReturnError(octetCount != 0, CHIP_ERROR_NO_MEMORY);
+
+        size_t octetCount;
+        ReturnErrorOnFailure(HexToBytes(
+            chip::CharSpan(hexData, hexDataLen),
+            [&buffer](size_t allocSize) {
+                buffer.Calloc(allocSize);
+                return buffer.Get();
+            },
+            &octetCount));
 
         return chip::app::DataModel::Encode(*writer, tag, chip::ByteSpan(buffer.Get(), octetCount));
     }
diff --git a/examples/chip-tool/commands/common/Command.cpp b/examples/chip-tool/commands/common/Command.cpp
index c4161b6..cd39293 100644
--- a/examples/chip-tool/commands/common/Command.cpp
+++ b/examples/chip-tool/commands/common/Command.cpp
@@ -18,6 +18,7 @@
 
 #include "Command.h"
 #include "CustomStringPrefix.h"
+#include "HexConversion.h"
 #include "platform/PlatformManager.h"
 
 #include <functional>
@@ -377,14 +378,16 @@
                 // representation is always longer than the octet string it encodes,
                 // so we have enough space in argValue for the decoded version.
                 chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
-                if (!buffer.Calloc(argLen)) // Bigger than needed, but it's fine.
-                {
-                    return false;
-                }
 
-                size_t octetCount =
-                    chip::Encoding::HexToBytes(argValue + kHexStringPrefixLen, argLen - kHexStringPrefixLen, buffer.Get(), argLen);
-                if (octetCount == 0)
+                size_t octetCount;
+                CHIP_ERROR err = HexToBytes(
+                    chip::CharSpan(argValue + kHexStringPrefixLen, argLen - kHexStringPrefixLen),
+                    [&buffer](size_t allocSize) {
+                        buffer.Calloc(allocSize);
+                        return buffer.Get();
+                    },
+                    &octetCount);
+                if (err != CHIP_NO_ERROR)
                 {
                     return false;
                 }
diff --git a/examples/chip-tool/commands/common/HexConversion.h b/examples/chip-tool/commands/common/HexConversion.h
new file mode 100644
index 0000000..b315681
--- /dev/null
+++ b/examples/chip-tool/commands/common/HexConversion.h
@@ -0,0 +1,65 @@
+/*
+ *   Copyright (c) 2022 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   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.
+ */
+
+#pragma once
+
+#include <lib/core/CHIPError.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/Span.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+/**
+ * Utility for converting a hex string to bytes, with the right error checking
+ * and allocation size computation.
+ *
+ * Takes a functor to allocate the buffer to use for the hex bytes.  The functor
+ * is expected to return uint8_t *.  The caller is responsible for cleaning up
+ * this buffer as needed.
+ *
+ * On success, *octetCount is filled with the number of octets placed in the
+ * buffer.  On failure, the value of *octetCount is undefined.
+ */
+template <typename F>
+CHIP_ERROR HexToBytes(chip::CharSpan hex, F bufferAllocator, size_t * octetCount)
+{
+    *octetCount = 0;
+
+    if (hex.size() % 2 != 0)
+    {
+        ChipLogError(chipTool, "Error while encoding '%.*s' as an octet string: Odd number of characters.",
+                     static_cast<int>(hex.size()), hex.data());
+        return CHIP_ERROR_INVALID_STRING_LENGTH;
+    }
+
+    const size_t bufferSize = hex.size() / 2;
+    uint8_t * buffer        = bufferAllocator(bufferSize);
+    if (buffer == nullptr && bufferSize != 0)
+    {
+        ChipLogError(chipTool, "Failed to allocate buffer of size: %llu", static_cast<unsigned long long>(bufferSize));
+        return CHIP_ERROR_NO_MEMORY;
+    }
+
+    size_t byteCount = chip::Encoding::HexToBytes(hex.data(), hex.size(), buffer, bufferSize);
+    if (byteCount == 0 && hex.size() != 0)
+    {
+        ChipLogError(chipTool, "Error while encoding '%.*s' as an octet string.", static_cast<int>(hex.size()), hex.data());
+        return CHIP_ERROR_INTERNAL;
+    }
+
+    *octetCount = byteCount;
+    return CHIP_NO_ERROR;
+}
diff --git a/examples/darwin-framework-tool/BUILD.gn b/examples/darwin-framework-tool/BUILD.gn
index a677fc6..3e59a3c 100644
--- a/examples/darwin-framework-tool/BUILD.gn
+++ b/examples/darwin-framework-tool/BUILD.gn
@@ -124,6 +124,7 @@
     "${chip_root}/examples/chip-tool/commands/common/Command.h",
     "${chip_root}/examples/chip-tool/commands/common/Commands.cpp",
     "${chip_root}/examples/chip-tool/commands/common/Commands.h",
+    "${chip_root}/examples/chip-tool/commands/common/HexConversion.h",
     "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/ComplexArgumentParser.cpp",
     "commands/clusters/ClusterCommandBridge.h",
     "commands/clusters/ModelCommandBridge.mm",
diff --git a/examples/tv-casting-app/tv-casting-common/BUILD.gn b/examples/tv-casting-app/tv-casting-common/BUILD.gn
index 37572da..4d797a9 100644
--- a/examples/tv-casting-app/tv-casting-common/BUILD.gn
+++ b/examples/tv-casting-app/tv-casting-common/BUILD.gn
@@ -43,6 +43,7 @@
     "${chip_root}/examples/chip-tool/commands/common/Commands.cpp",
     "${chip_root}/examples/chip-tool/commands/common/Commands.h",
     "${chip_root}/examples/chip-tool/commands/common/CredentialIssuerCommands.h",
+    "${chip_root}/examples/chip-tool/commands/common/HexConversion.h",
     "${chip_root}/src/controller/ExamplePersistentStorage.cpp",
     "${chip_root}/src/controller/ExamplePersistentStorage.h",
     "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/ComplexArgumentParser.cpp",