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

Issue:
XML deserialization fails when empty `hashes` collection is provided
Deserializing a BOM in XML format fails, when it contains empty `hashes` collections (`<hashes />`).

The following BOM can be used to reproduce the issue:

```xml
<?xml version="1.0"?>
<bom serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" version="1" xmlns="http://cyclonedx.org/schema/bom/1.4">
    <components>
        <component type="application">
            <name>tomcat-catalina</name>
            <version>9.0.14</version>
            <hashes />
        </component>
    </components>
</bom>
```

Stacktrace:

```
java.lang.NullPointerException: Cannot invoke "com.fasterxml.jackson.databind.JsonNode.asText()" because the return value of "com.fasterxml.jackson.databind.JsonNode.get(String)" is null
	at org.cyclonedx.util.deserializer.HashesDeserializer.parseHash(HashesDeserializer.java:45)
	at org.cyclonedx.util.deserializer.HashesDeserializer.parseHashes(HashesDeserializer.java:38)
	at org.cyclonedx.util.deserializer.HashesDeserializer.deserialize(HashesDeserializer.java:25)
	at org.cyclonedx.util.deserializer.HashesDeserializer.deserialize(HashesDeserializer.java:15)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
```

Pull request description:
Fix failure to deserialize empty `hashes` node
Fixes #343

Patch:
diff --git a/src/main/java/org/cyclonedx/util/deserializer/HashesDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/HashesDeserializer.java
index f1576ab9d..1f391550c 100644
--- a/src/main/java/org/cyclonedx/util/deserializer/HashesDeserializer.java
+++ b/src/main/java/org/cyclonedx/util/deserializer/HashesDeserializer.java
@@ -2,6 +2,7 @@ package org.cyclonedx.util.deserializer;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import com.fasterxml.jackson.core.JsonParser;
@@ -26,6 +27,10 @@ public class HashesDeserializer
   }
 
   private List<Hash> parseHashes(JsonNode node) {
+    if (node.isEmpty()) {
+      return Collections.emptyList();
+    }
+
     List<Hash> hashes = new ArrayList<>();
     ArrayNode nodes = (node.isArray() ? (ArrayNode) node : new ArrayNode(null).add(node));
 
@@ -37,7 +42,10 @@ public class HashesDeserializer
   }
 
   private Hash parseHash(JsonNode node) {
-    String alg = node.get("alg").asText();
+    String alg = null;
+    if (node.has("alg")) {
+      alg = node.get("alg").asText();
+    }
 
     String value = null;
     JsonNode valueNode = node.get("content");


Test patch:
diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
index 4091e4d8c..893116628 100644
--- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
@@ -208,4 +208,10 @@ public class JsonParserTest
         assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
         assertEquals("acme-tool-b", bom.getMetadata().getTools().get(1).getName());
     }
+
+    @Test
+    public void testIssue343Regression() throws Exception {
+        final Bom bom = getJsonBom("regression/issue343-empty-hashes.json");
+        assertEquals(0, bom.getComponents().get(0).getHashes().size());
+    }
 }
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index ccbcabbef..a7568e86b 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -361,4 +361,10 @@ public class XmlParserTest
         assertEquals("acme-tool-a", bom.getMetadata().getTools().get(0).getName());
         assertEquals("acme-tool-b", bom.getMetadata().getTools().get(1).getName());
     }
+
+    @Test
+    public void testIssue343Regression() throws Exception {
+        final Bom bom = getXmlBom("regression/issue343-empty-hashes.xml");
+        assertEquals(0, bom.getComponents().get(0).getHashes().size());
+    }
 }
diff --git a/src/test/resources/regression/issue343-empty-hashes.json b/src/test/resources/regression/issue343-empty-hashes.json
new file mode 100644
index 000000000..0168b2dc9
--- /dev/null
+++ b/src/test/resources/regression/issue343-empty-hashes.json
@@ -0,0 +1,13 @@
+{
+  "bomFormat" : "CycloneDX",
+  "specVersion" : "1.4",
+  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
+  "version" : 1,
+  "components": [
+    {
+      "type": "application",
+      "name": "acme-app",
+      "hashes": []
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/regression/issue343-empty-hashes.xml b/src/test/resources/regression/issue343-empty-hashes.xml
new file mode 100644
index 000000000..9d85e40a7
--- /dev/null
+++ b/src/test/resources/regression/issue343-empty-hashes.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<bom serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" version="1" xmlns="http://cyclonedx.org/schema/bom/1.4">
+    <components>
+        <component type="application">
+            <name>acme-app</name>
+            <hashes />
+        </component>
+    </components>
+</bom>
\ No newline at end of file