From 05088c3070766fa1f1f4443ccb5bf4df44816202 Mon Sep 17 00:00:00 2001 From: andreas hilti Date: Sat, 25 May 2024 20:49:46 +0200 Subject: [PATCH 1/2] improve merge performance Signed-off-by: andreas hilti --- .../Json/Serializer.Serialization.cs | 13 +++++++++ src/CycloneDX.Core/Models/Composition.cs | 12 +++++++- .../Models/ExternalReference.cs | 14 +++++++++- src/CycloneDX.Utils/Merge.cs | 28 +++++++++++++++++-- tests/CycloneDX.Utils.Tests/MergeTests.cs | 25 +++++++++++++++++ 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index 921d96ca..d4cd2063 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -89,5 +89,18 @@ internal static string Serialize(Models.Vulnerabilities.Vulnerability vulnerabil Contract.Requires(vulnerability != null); return JsonSerializer.Serialize(vulnerability, _options); } + + internal static string Serialize(Models.Composition composition) + { + Contract.Requires(composition != null); + return JsonSerializer.Serialize(composition, _options); + } + + internal static string Serialize(Models.ExternalReference externalReference) + { + Contract.Requires(externalReference != null); + return JsonSerializer.Serialize(externalReference, _options); + } + } } diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index 8b3140d4..cad7d19a 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Composition : IXmlSerializable + public class Composition : IXmlSerializable, IEquatable { [ProtoContract] public enum AggregateType @@ -194,5 +194,15 @@ public void WriteXml(System.Xml.XmlWriter writer) { writer.WriteEndElement(); } } + + public bool Equals(Composition obj) + { + return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + } } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index e84ef997..be194c0a 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -15,8 +15,10 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -24,7 +26,7 @@ namespace CycloneDX.Models { [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores")] [ProtoContract] - public class ExternalReference + public class ExternalReference : IEquatable { [ProtoContract] public enum ExternalReferenceType @@ -125,5 +127,15 @@ public enum ExternalReferenceType [ProtoMember(4)] public List Hashes { get; set; } public bool ShouldSerializeHashes() { return Hashes?.Count > 0; } + + public bool Equals(ExternalReference obj) + { + return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index da18352c..36c19596 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Utils { - class ListMergeHelper + class ListMergeHelper where T : IEquatable { public List Merge(List list1, List list2) { @@ -31,12 +31,36 @@ public List Merge(List list1, List list2) if (list2 is null) return list1; var result = new List(list1); + // We want to avoid the costly computation of the hashes if possible. + // Therefore, we use a nullable type. + var resultHashes = new List(list1.Count); + for (int i = 0; i < list1.Count; i++) + { + resultHashes.Add(null); + } foreach (var item in list2) { - if (!(result.Contains(item))) + int hash = item.GetHashCode(); + bool found = false; + for (int i = 0; i < result.Count; i++) + { + var resultItem = result[i]; + if (resultHashes[i] == null) + { + resultHashes[i] = resultItem.GetHashCode(); + } + int resultHash = resultHashes[i].Value; + if (hash == resultHash && item.Equals(resultItem)) + { + found = true; + break; + } + } + if (!found) { result.Add(item); + resultHashes.Add(hash); } } diff --git a/tests/CycloneDX.Utils.Tests/MergeTests.cs b/tests/CycloneDX.Utils.Tests/MergeTests.cs index 7564ff18..186e1cf7 100644 --- a/tests/CycloneDX.Utils.Tests/MergeTests.cs +++ b/tests/CycloneDX.Utils.Tests/MergeTests.cs @@ -105,6 +105,31 @@ public void FlatMergeComponentsTest() Snapshot.Match(result); } + [Fact] + public void FlatMergeDuplicatedComponentsTest() + { + var sboms = new List(); + for (int i = 0; i < 3; i++) + { + var bom = new Bom + { + Components = new List + { + new Component + { + Name = "Component1", + Version = "1" + } + } + }; + sboms.Add(bom); + } + var result = CycloneDXUtils.FlatMerge(sboms); + + Assert.Single(result.Components); + } + + [Fact] public void FlatMergeVulnerabilitiesTest() { From 078e59a0255c1390e767030e36987d445afbf3de Mon Sep 17 00:00:00 2001 From: andreas hilti Date: Thu, 6 Jun 2024 21:33:45 +0200 Subject: [PATCH 2/2] Override also Equals(object) Signed-off-by: andreas hilti --- src/CycloneDX.Core/Models/Component.cs | 11 +++++++++++ src/CycloneDX.Core/Models/Composition.cs | 11 +++++++++++ src/CycloneDX.Core/Models/Dependency.cs | 11 +++++++++++ src/CycloneDX.Core/Models/ExternalReference.cs | 11 +++++++++++ src/CycloneDX.Core/Models/Service.cs | 11 +++++++++++ src/CycloneDX.Core/Models/Tool.cs | 11 +++++++++++ .../Models/Vulnerabilities/Vulnerability.cs | 13 ++++++++++++- 7 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 2802ef26..824309ac 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -218,6 +218,17 @@ public bool NonNullableModified [ProtoMember(26)] public Data Data { get; set; } + public override bool Equals(object obj) + { + var other = obj as Component; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Component obj) { return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index cad7d19a..cf5cb654 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -195,6 +195,17 @@ public void WriteXml(System.Xml.XmlWriter writer) { } } + public override bool Equals(object obj) + { + var other = obj as Composition; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Composition obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index f2bd35c0..7b0fe510 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -36,6 +36,17 @@ public class Dependency: IEquatable [ProtoMember(2)] public List Dependencies { get; set; } + public override bool Equals(object obj) + { + var other = obj as Dependency; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Dependency obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index be194c0a..69204fca 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -128,6 +128,17 @@ public enum ExternalReferenceType public List Hashes { get; set; } public bool ShouldSerializeHashes() { return Hashes?.Count > 0; } + public override bool Equals(object obj) + { + var other = obj as ExternalReference; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(ExternalReference obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index fda223f7..a8d82ada 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -194,6 +194,17 @@ public ServiceDataChoices XmlData public bool ShouldSerializeProperties() => Properties?.Count > 0; + public override bool Equals(object obj) + { + var other = obj as Service; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Service obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 98349490..48282a17 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -48,6 +48,17 @@ public class Tool: IEquatable public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } + public override bool Equals(object obj) + { + var other = obj as Tool; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Tool obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index 205d263d..83b6b68b 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -143,7 +143,18 @@ public DateTime? Rejected [ProtoMember(18)] public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } - + + public override bool Equals(object obj) + { + var other = obj as Vulnerability; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + public bool Equals(Vulnerability obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj);