Patch Detail
Show a patch.
GET /api/1.1/patches/21289/?format=api
{ "id": 21289, "url": "https://patchwork.libcamera.org/api/1.1/patches/21289/?format=api", "web_url": "https://patchwork.libcamera.org/patch/21289/", "project": { "id": 1, "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api", "name": "libcamera", "link_name": "libcamera", "list_id": "libcamera_core", "list_email": "libcamera-devel@lists.libcamera.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<20240919141832.40249-1-stefan.klug@ideasonboard.com>", "date": "2024-09-19T14:17:41", "name": "[v2] libcamera: yaml-parser: Differentiate between empty and empty string", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": false, "hash": "8672d9b0b94a283e9280db1fa0216f6f7eeb6792", "submitter": { "id": 184, "url": "https://patchwork.libcamera.org/api/1.1/people/184/?format=api", "name": "Stefan Klug", "email": "stefan.klug@ideasonboard.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/21289/mbox/", "series": [ { "id": 4606, "url": "https://patchwork.libcamera.org/api/1.1/series/4606/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4606", "date": "2024-09-19T14:17:41", "name": "[v2] libcamera: yaml-parser: Differentiate between empty and empty string", "version": 2, "mbox": "https://patchwork.libcamera.org/series/4606/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/21289/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/21289/checks/", "tags": {}, "headers": { "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>", "X-Original-To": "parsemail@patchwork.libcamera.org", "Delivered-To": "parsemail@patchwork.libcamera.org", "Received": [ "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 21BF7C324C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 19 Sep 2024 14:18:42 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id BD77F634FC;\n\tThu, 19 Sep 2024 16:18:40 +0200 (CEST)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id C2134618E7\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 19 Sep 2024 16:18:39 +0200 (CEST)", "from ideasonboard.com (unknown\n\t[IPv6:2a01:599:702:4194:8814:3530:315c:ff1e])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 40AF1220;\n\tThu, 19 Sep 2024 16:17:16 +0200 (CEST)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"PaTEPuK2\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1726755436;\n\tbh=RQulZ8m9uOF7xxFPLzTuA0vFkHGXrTXNSEXzYBctYFY=;\n\th=From:To:Cc:Subject:Date:From;\n\tb=PaTEPuK26b5qu1aTD7ZB4RDkvJQYbWLDm+Aq/m9tiMnIC0sIoQWyaHxW2Z4q4nNU/\n\tZr1XtiD7JmL/p3Ez4yCnMV7Fx1mEkaMiChRMmKKNq0Pta/p8LqjVvAg8NEYDfAsp3d\n\t09kuB/tA6PqEXlts2hUFF3qP0LVxAxgSLhlmLbQw=", "From": "Stefan Klug <stefan.klug@ideasonboard.com>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Stefan Klug <stefan.klug@ideasonboard.com>", "Subject": "[PATCH v2] libcamera: yaml-parser: Differentiate between empty and\n\tempty string", "Date": "Thu, 19 Sep 2024 16:17:41 +0200", "Message-ID": "<20240919141832.40249-1-stefan.klug@ideasonboard.com>", "X-Mailer": "git-send-email 2.43.0", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "X-BeenThere": "libcamera-devel@lists.libcamera.org", "X-Mailman-Version": "2.1.29", "Precedence": "list", "List-Id": "<libcamera-devel.lists.libcamera.org>", "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>", "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>", "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>", "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>", "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>", "Errors-To": "libcamera-devel-bounces@lists.libcamera.org", "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>" }, "content": "When accessing a nonexistent key on a dict the YamlObject returns an\nempty element. This element can happily be casted to a string which is\nunexpected. For example the following statement:\n\nyamlDict[\"nonexistant\"].get<string>(\"default\")\n\nis expected to return \"default\" but actually returns \"\". Fix this by\nintroducing a empty type to distinguish between an empty YamlObject and\na YamlObject of type value containing an empty string. For completeness\nadd an isEmpty() function and an explicit cast to bool to be able to\ntest for that type.\n\nThe tests are adapted accordingly. Additional tests for empty entries in\nlists and dicts are added to ensure that these still get parsed as empty\nstrings. This is needed for algorithms without parameters to be parsed\ncorrectly from tuning files.\n\nSigned-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n\n---\nChanges in v2:\n- Added explicit to bool conversion operator\n- Fixed typos from review\n- I didn't add the rb tags, because the v2 adds quite some logic\n (especially tests) and this is a central piece of code\n---\n include/libcamera/internal/yaml_parser.h | 9 +++++\n src/libcamera/yaml_parser.cpp | 27 ++++++++++---\n test/yaml-parser.cpp | 51 ++++++++++++++++++++++--\n 3 files changed, 78 insertions(+), 9 deletions(-)", "diff": "diff --git a/include/libcamera/internal/yaml_parser.h b/include/libcamera/internal/yaml_parser.h\nindex 16708e488d88..6211ff4ae563 100644\n--- a/include/libcamera/internal/yaml_parser.h\n+++ b/include/libcamera/internal/yaml_parser.h\n@@ -159,6 +159,14 @@ public:\n \t{\n \t\treturn type_ == Type::Dictionary;\n \t}\n+\tbool isEmpty() const\n+\t{\n+\t\treturn type_ == Type::Empty;\n+\t}\n+\texplicit operator bool() const\n+\t{\n+\t\treturn type_ != Type::Empty;\n+\t}\n \n \tstd::size_t size() const;\n \n@@ -212,6 +220,7 @@ private:\n \t\tDictionary,\n \t\tList,\n \t\tValue,\n+\t\tEmpty,\n \t};\n \n \ttemplate<typename T>\ndiff --git a/src/libcamera/yaml_parser.cpp b/src/libcamera/yaml_parser.cpp\nindex 8b6a403898be..4784f2dc3d62 100644\n--- a/src/libcamera/yaml_parser.cpp\n+++ b/src/libcamera/yaml_parser.cpp\n@@ -38,12 +38,12 @@ static const YamlObject empty;\n * \\brief A class representing the tree structure of the YAML content\n *\n * The YamlObject class represents the tree structure of YAML content. A\n- * YamlObject can be a dictionary or list of YamlObjects or a value if a tree\n- * leaf.\n+ * YamlObject can be empty, a dictionary or list of YamlObjects, or a value if a\n+ * tree leaf.\n */\n \n YamlObject::YamlObject()\n-\t: type_(Type::Value)\n+\t: type_(Type::Empty)\n {\n }\n \n@@ -70,6 +70,20 @@ YamlObject::~YamlObject() = default;\n * \\return True if the YamlObject is a dictionary, false otherwise\n */\n \n+/**\n+ * \\fn YamlObject::isEmpty()\n+ * \\brief Return whether the YamlObject is an empty\n+ *\n+ * \\return True if the YamlObject is empty, false otherwise\n+ */\n+\n+/**\n+ * \\fn YamlObject::operator bool()\n+ * \\brief Return whether the YamlObject is a non-empty\n+ *\n+ * \\return False if the YamlObject is empty, true otherwise\n+ */\n+\n /**\n * \\fn YamlObject::size()\n * \\brief Retrieve the number of elements in a dictionary or list YamlObject\n@@ -443,7 +457,8 @@ template std::optional<std::vector<Size>> YamlObject::getList<Size>() const;\n *\n * This function retrieves an element of the YamlObject. Only YamlObject\n * instances of List type associate elements with index, calling this function\n- * on other types of instances is invalid and results in undefined behaviour.\n+ * on other types of instances or with an invalid index results in an empty\n+ * object.\n *\n * \\return The YamlObject as an element of the list\n */\n@@ -480,8 +495,8 @@ bool YamlObject::contains(const std::string &key) const\n *\n * This function retrieve a member of a YamlObject by name. Only YamlObject\n * instances of Dictionary type associate elements with names, calling this\n- * function on other types of instances is invalid and results in undefined\n- * behaviour.\n+ * function on other types of instances or with a nonexistent key results in an\n+ * empty object.\n *\n * \\return The YamlObject corresponding to the \\a key member\n */\ndiff --git a/test/yaml-parser.cpp b/test/yaml-parser.cpp\nindex 81c829834667..9d340e29d4a9 100644\n--- a/test/yaml-parser.cpp\n+++ b/test/yaml-parser.cpp\n@@ -34,10 +34,12 @@ static const string testYaml =\n \t\"list:\\n\"\n \t\" - James\\n\"\n \t\" - Mary\\n\"\n+\t\" - \\n\"\n \t\"dictionary:\\n\"\n \t\" a: 1\\n\"\n \t\" c: 3\\n\"\n \t\" b: 2\\n\"\n+\t\" empty:\\n\"\n \t\"level1:\\n\"\n \t\" level2:\\n\"\n \t\" - [1, 2]\\n\"\n@@ -430,9 +432,10 @@ protected:\n \t\tif (testObjectType(listObj, \"list\", Type::List) != TestPass)\n \t\t\treturn TestFail;\n \n-\t\tstatic constexpr std::array<const char *, 2> listValues{\n+\t\tstatic constexpr std::array<const char *, 3> listValues{\n \t\t\t\"James\",\n \t\t\t\"Mary\",\n+\t\t\t\"\",\n \t\t};\n \n \t\tif (listObj.size() != listValues.size()) {\n@@ -465,16 +468,22 @@ protected:\n \t\t\ti++;\n \t\t}\n \n+\t\t/* Ensure that empty objects get parsed as empty strings. */\n+\t\tif (!listObj[2].isValue()) {\n+\t\t\tcerr << \"Empty object is not a value\" << std::endl;\n+\t\t}\n+\n \t\t/* Test dictionary object */\n \t\tauto &dictObj = (*root)[\"dictionary\"];\n \n \t\tif (testObjectType(dictObj, \"dictionary\", Type::Dictionary) != TestPass)\n \t\t\treturn TestFail;\n \n-\t\tstatic constexpr std::array<std::pair<const char *, int>, 3> dictValues{ {\n+\t\tstatic constexpr std::array<std::pair<const char *, int>, 4> dictValues{ {\n \t\t\t{ \"a\", 1 },\n \t\t\t{ \"c\", 3 },\n \t\t\t{ \"b\", 2 },\n+\t\t\t{ \"empty\", -100 },\n \t\t} };\n \n \t\tsize_t dictSize = dictValues.size();\n@@ -505,7 +514,7 @@ protected:\n \t\t\t\treturn TestFail;\n \t\t\t}\n \n-\t\t\tif (elem.get<int32_t>(0) != item.second) {\n+\t\t\tif (elem.get<int32_t>(-100) != item.second) {\n \t\t\t\tstd::cerr << \"Dictionary element \" << i << \" has wrong value\"\n \t\t\t\t\t << std::endl;\n \t\t\t\treturn TestFail;\n@@ -514,6 +523,42 @@ protected:\n \t\t\ti++;\n \t\t}\n \n+\t\t/* Ensure that empty objects get parsed as empty strings. */\n+\t\tif (!dictObj[\"empty\"].isValue()) {\n+\t\t\tcerr << \"Empty object is not of type value\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Ensure that keys without values are still added to a dict. */\n+\t\tif (!dictObj.contains(\"empty\")) {\n+\t\t\tcerr << \"Empty element is missing in dict\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test explicit cast to bool on an empty object must return true. */\n+\t\tif (!!dictObj[\"empty\"] != true) {\n+\t\t\tcerr << \"Casting empty entry to bool returns false\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test nonexistent object has value type empty. */\n+\t\tif (!dictObj[\"nonexistent\"].isEmpty()) {\n+\t\t\tcerr << \"Accessing nonexistent object returns non-empty object\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test access to nonexistent member. */\n+\t\tif (dictObj[\"nonexistent\"].get<std::string>(\"default\") != \"default\") {\n+\t\t\tcerr << \"Accessing nonexistent dict entry fails to return default\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test explicit cast to bool on nonexistent object returns false. */\n+\t\tif (!!dictObj[\"nonexistent\"] != false) {\n+\t\t\tcerr << \"Casting nonexistent dict entry to bool returns true\" << std::endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n \t\t/* Make sure utils::map_keys() works on the adapter. */\n \t\t(void)utils::map_keys(dictObj.asDict());\n \n", "prefixes": [ "v2" ] }