Repository: CycloneDX/cyclonedx-core-java (java)

Issue:
XML deserialization fails when multiple legacy `Tool`s are provided
Originally reported in the Dependency-Track repository: https://github.com/DependencyTrack/dependency-track/issues/3107

When an XML BOM contains multiple `metadata>tools>tool` nodes, deserialization fails with:

```
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `org.cyclonedx.model.Tool` from Array value (token `JsonToken.START_ARRAY`)
 at [Source: UNKNOWN; byte offset: #UNKNOWN]
        at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
        at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1752)
        at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1526)
        at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1473)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeFromArray(BeanDeserializer.java:656)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:211)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4539)
```

Reproducible with the following BOM:

```xml
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     serialNumber="urn:uuid:1624fa6f-aebe-4dba-8ead-f2c876c9b832" version="1"
     xmlns="http://cyclonedx.org/schema/bom/1.4">
    <metadata>
        <timestamp>2023-10-17T08:58:32.033Z</timestamp>
        <tools>
            <tool>
                <name>acme-tool-a</name>
                <version>1.0.0</version>
            </tool>
            <tool>
                <name>acme-tool-b</name>
                <version>2.0.0</version>
            </tool>
        </tools>
    </metadata>
</bom>
```

Pull request description:
Fix XML deserialization of legacy `Tool`s
Fixes #338

Patch:
diff --git a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java
index ce10becb6..0314ac251 100644
--- a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java
+++ b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java
@@ -105,8 +105,19 @@ public class MetadataDeserializer
         metadata.setTools(tools);
       }
       else if (toolsNode.has("tool")) {
-        Tool tool = mapper.convertValue(toolsNode.get("tool"), Tool.class);
-        metadata.setTools(Collections.singletonList(tool));
+        final JsonNode toolNode = toolsNode.get("tool");
+        // When deserializing XML BOMs, and multiple tools are provided, Jackson's internal
+        // representation looks like this:
+        //   {"tool": [{"name": "foo"}, {"name": "bar"}]}
+        // If only a single tool is provided, it looks like this:
+        //   {"tool": {"name": "foo"}}
+        if (toolNode.isArray()) {
+          List<Tool> tools = mapper.convertValue(toolsNode.get("tool"), new TypeReference<List<Tool>>() { });
+          metadata.setTools(tools);
+        } else {
+          Tool tool = mapper.convertValue(toolsNode.get("tool"), Tool.class);
+          metadata.setTools(Collections.singletonList(tool));
+        }
       }
       else {
         ToolInformation toolInformation = new ToolInformation();


Test patch:
diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
index bf062158a..790a36435 100644
--- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
@@ -185,4 +185,17 @@ public class JsonParserTest
         assertCommonBomProperties(bom, Version.VERSION_15);
         assertMetadata_validTools(bom.getMetadata());
     }
+
+    @Test
+    public void testIssue338RegressionWithSingleTool() throws Exception {
+        final Bom bom = getJsonBom("regression/issue338-single-tool.json");
+        assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
+    }
+
+    @Test
+    public void testIssue338RegressionWithMultipleTools() throws Exception {
+        final Bom bom = getJsonBom("regression/issue338-multiple-tools.json");
+        assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
+        assertEquals("acme-tool-b", bom.getMetadata().getTools().get(1).getName());
+    }
 }
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index a1b2d9e92..845d2d48e 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -341,4 +341,17 @@ public class XmlParserTest
         assertEquals(1, bom.getVersion());
         assertNull(bom.getVulnerabilities());
     }
+
+    @Test
+    public void testIssue338RegressionWithSingleTool() throws Exception {
+        final Bom bom = getXmlBom("regression/issue338-single-tool.xml");
+        assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
+    }
+
+    @Test
+    public void testIssue338RegressionWithMultipleTools() throws Exception {
+        final Bom bom = getXmlBom("regression/issue338-multiple-tools.xml");
+        assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
+        assertEquals("acme-tool-b", bom.getMetadata().getTools().get(1).getName());
+    }
 }
diff --git a/src/test/resources/regression/issue338-multiple-tools.json b/src/test/resources/regression/issue338-multiple-tools.json
new file mode 100644
index 000000000..52ed13217
--- /dev/null
+++ b/src/test/resources/regression/issue338-multiple-tools.json
@@ -0,0 +1,16 @@
+{
+  "bomFormat" : "CycloneDX",
+  "specVersion" : "1.4",
+  "serialNumber": "urn:uuid:1624fa6f-aebe-4dba-8ead-f2c876c9b832",
+  "version" : 1,
+  "metadata": {
+    "tools": [
+      {
+        "name": "acme-tool-a"
+      },
+      {
+        "name": "acme-tool-b"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/regression/issue338-multiple-tools.xml b/src/test/resources/regression/issue338-multiple-tools.xml
new file mode 100644
index 000000000..c6f354873
--- /dev/null
+++ b/src/test/resources/regression/issue338-multiple-tools.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+     serialNumber="urn:uuid:1624fa6f-aebe-4dba-8ead-f2c876c9b832" version="1"
+     xmlns="http://cyclonedx.org/schema/bom/1.4">
+    <metadata>
+        <tools>
+            <tool>
+                <name>acme-tool-a</name>
+                <version>1.0.0</version>
+            </tool>
+            <tool>
+                <name>acme-tool-b</name>
+                <version>2.0.0</version>
+            </tool>
+        </tools>
+    </metadata>
+</bom>
\ No newline at end of file
diff --git a/src/test/resources/regression/issue338-single-tool.json b/src/test/resources/regression/issue338-single-tool.json
new file mode 100644
index 000000000..ff5fa2774
--- /dev/null
+++ b/src/test/resources/regression/issue338-single-tool.json
@@ -0,0 +1,13 @@
+{
+  "bomFormat" : "CycloneDX",
+  "specVersion" : "1.4",
+  "serialNumber": "urn:uuid:1624fa6f-aebe-4dba-8ead-f2c876c9b832",
+  "version" : 1,
+  "metadata": {
+    "tools": [
+      {
+        "name": "acme-tool-a"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/regression/issue338-single-tool.xml b/src/test/resources/regression/issue338-single-tool.xml
new file mode 100644
index 000000000..e157dc260
--- /dev/null
+++ b/src/test/resources/regression/issue338-single-tool.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+     serialNumber="urn:uuid:1624fa6f-aebe-4dba-8ead-f2c876c9b832" version="1"
+     xmlns="http://cyclonedx.org/schema/bom/1.4">
+    <metadata>
+        <tools>
+            <tool>
+                <name>acme-tool-a</name>
+                <version>1.0.0</version>
+            </tool>
+        </tools>
+    </metadata>
+</bom>
\ No newline at end of file