From fb1056e7d71344d9345fe3a0d5056a3b81b1145f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 14:56:39 +0200 Subject: [PATCH 001/285] Json/Validator: add findNamedElements() to detect duplicate "bom-ref" definitions Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 04506afe..dd4a5e57 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,6 +166,88 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) + { + if (dict2 == null || dict2.Count == 0) + { + return dict1; + } + + if (dict1 == null || dict1.Count == 0) + { + return dict2; + } + + foreach (KeyValuePair> KVP in dict2) + { + if (dict1.ContainsKey(KVP.Key)) + { + dict1[KVP.Key].AddRange(KVP.Value); + } + else + { + dict1.Add(KVP.Key, KVP.Value); + } + } + + return dict1; + } + + /// + /// Iterate through the JSON document to find JSON objects whose property names + /// match the one we seek, and add such hits to returned list. Recurse and repeat. + /// + /// A JsonElement, starting from JsonDocument.RootElement + /// for the original caller, probably. Then used to recurse. + /// + /// The property name we seek. + /// A Dictionary with distinct values of the seeked JsonElement as keys, + /// and a List of "parent" JsonElement (which contain such key) as mapped values. + /// + private static Dictionary> findNamedElements(JsonElement element, string name) + { + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; + + // Can we iterate further? + switch (element.ValueKind) { + case JsonValueKind.Object: + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.Name == name) { + if (!hits.ContainsKey(property.Value)) + { + hits.Add(property.Value, new List()); + } + hits[property.Value].Add(element); + } + + // Note: Here we can recurse into same property that + // we've just listed, if it is not of a simple kind. + nestedHits = findNamedElements(property.Value, name); + hits = addDictList(hits, nestedHits); + } + break; + + case JsonValueKind.Array: + foreach (JsonElement nestedElem in element.EnumerateArray()) + { + nestedHits = findNamedElements(nestedElem, name); + hits = addDictList(hits, nestedHits); + } + break; + + default: + // No-op for simple types: these values per se have no name + // to learn, and we can not iterate deeper into them. + break; + } + + return hits; + } + private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDocument, string schemaVersionString) { var validationMessages = new List(); @@ -190,6 +272,23 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc } } } + + // The JSON Schema, at least the ones defined by CycloneDX + // and handled by current parser in dotnet ecosystem, can + // not specify or check the uniqueness requirement for the + // "bom-ref" assignments in the overall document (e.g. in + // "metadata/component" and list of "components", as well + // as in "services" and "vulnerabilities", as of CycloneDX + // spec v1.4), so this is checked separately here if the + // document seems structurally intact otherwise. + // Note that this is not a problem for the XML schema with + // its explicit constraint. + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + if (KVP.Value != null && KVP.Value.Count != 1) { + validationMessages.Add($"'bom-ref' value of {KVP.Key.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + } + } } else { From 3fea2a11bb8168c3763e0e4d1e582358a9903772 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 17:44:21 +0200 Subject: [PATCH 002/285] Json/Validator.cs: when checking for (bom-ref) uniqueness, consider string representation of the JsonElement, not object (which is in fact unique for each node) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 39 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index dd4a5e57..681106d7 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,9 +166,9 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } - private static Dictionary> addDictList( - Dictionary> dict1, - Dictionary> dict2) + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) { if (dict2 == null || dict2.Count == 0) { @@ -180,10 +180,12 @@ private static Dictionary> addDictList( return dict2; } - foreach (KeyValuePair> KVP in dict2) + foreach (KeyValuePair> KVP in dict2) { if (dict1.ContainsKey(KVP.Key)) { + // NOTE: Possibly different object, but same string representation! + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -203,25 +205,29 @@ private static Dictionary> addDictList( /// for the original caller, probably. Then used to recurse. /// /// The property name we seek. - /// A Dictionary with distinct values of the seeked JsonElement as keys, - /// and a List of "parent" JsonElement (which contain such key) as mapped values. + /// A Dictionary with distinct values of string representation of the + /// seeked JsonElement as keys, and a List of actual JsonElement objects as + /// mapped values. /// - private static Dictionary> findNamedElements(JsonElement element, string name) + private static Dictionary> findNamedElements(JsonElement element, string name) { - Dictionary> hits = new Dictionary>(); - Dictionary> nestedHits = null; + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; // Can we iterate further? switch (element.ValueKind) { case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - if (!hits.ContainsKey(property.Value)) + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); + string key = property.Value.ToString(); + if (!(hits.ContainsKey(key))) { - hits.Add(property.Value, new List()); + hits.Add(key, new List()); } - hits[property.Value].Add(element); + hits[key].Add(property.Value); } // Note: Here we can recurse into same property that @@ -232,6 +238,7 @@ private static Dictionary> findNamedElements(Json break; case JsonValueKind.Array: + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -283,10 +290,12 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // document seems structurally intact otherwise. // Note that this is not a problem for the XML schema with // its explicit constraint. - Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); - foreach (KeyValuePair> KVP in bomRefs) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { - validationMessages.Add($"'bom-ref' value of {KVP.Key.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } } } From c7f4ba202b74b40bfb649ddc09a514f3bc8b80bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 18:09:07 +0200 Subject: [PATCH 003/285] Json/Validator.cs: drop #COMMENTED-DEBUG# for findNamedElements() troubleshooting Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 681106d7..0d2cc76b 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -185,7 +185,6 @@ private static Dictionary> addDictList( if (dict1.ContainsKey(KVP.Key)) { // NOTE: Possibly different object, but same string representation! - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -219,9 +218,7 @@ private static Dictionary> findNamedElements(JsonEleme case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); string key = property.Value.ToString(); if (!(hits.ContainsKey(key))) { @@ -238,7 +235,6 @@ private static Dictionary> findNamedElements(JsonEleme break; case JsonValueKind.Array: - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -290,10 +286,8 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // document seems structurally intact otherwise. // Note that this is not a problem for the XML schema with // its explicit constraint. - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); foreach (KeyValuePair> KVP in bomRefs) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } From e990cd2d85d1743adb64a3686796f482216fa22b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 21:56:55 +0000 Subject: [PATCH 004/285] Bump coverlet.collector from 3.2.0 to 6.0.0 Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 3.2.0 to 6.0.0. - [Release notes](https://github.com/coverlet-coverage/coverlet/releases) - [Commits](https://github.com/coverlet-coverage/coverlet/compare/v3.2.0...v6.0.0) --- updated-dependencies: - dependency-name: coverlet.collector dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj | 2 +- .../CycloneDX.Spdx.Interop.Tests.csproj | 2 +- tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj | 2 +- tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj b/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj index 421c9c74..7f496795 100644 --- a/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj +++ b/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj @@ -18,7 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj b/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj index 2fd6d085..5719eea2 100644 --- a/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj +++ b/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj b/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj index 6d54f265..617ab130 100644 --- a/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj +++ b/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj b/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj index b5e190be..e3184a00 100644 --- a/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj +++ b/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From b4a9491a1eeb4e3439b67c76eb7269f353713547 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 21:56:43 +0000 Subject: [PATCH 005/285] Bump protobuf-net from 3.2.16 to 3.2.26 Bumps [protobuf-net](https://github.com/protobuf-net/protobuf-net) from 3.2.16 to 3.2.26. - [Changelog](https://github.com/protobuf-net/protobuf-net/blob/main/docs/releasenotes.md) - [Commits](https://github.com/protobuf-net/protobuf-net/commits) --- updated-dependencies: - dependency-name: protobuf-net dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/CycloneDX.Core/CycloneDX.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/CycloneDX.Core.csproj b/src/CycloneDX.Core/CycloneDX.Core.csproj index df22c07f..3ea6180f 100644 --- a/src/CycloneDX.Core/CycloneDX.Core.csproj +++ b/src/CycloneDX.Core/CycloneDX.Core.csproj @@ -13,7 +13,7 @@ all - + From e9b5359cc9cd04750017df43637301eff255592f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 21:56:49 +0000 Subject: [PATCH 006/285] Bump Microsoft.NET.Test.Sdk from 17.5.0 to 17.6.3 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.5.0 to 17.6.3. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.5.0...v17.6.3) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj | 2 +- .../CycloneDX.Spdx.Interop.Tests.csproj | 2 +- tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj | 2 +- tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj b/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj index 7f496795..8c0d7108 100644 --- a/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj +++ b/tests/CycloneDX.Core.Tests/CycloneDX.Core.Tests.csproj @@ -11,7 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj b/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj index 5719eea2..9a00c99c 100644 --- a/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj +++ b/tests/CycloneDX.Spdx.Interop.Tests/CycloneDX.Spdx.Interop.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj b/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj index 617ab130..889d5676 100644 --- a/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj +++ b/tests/CycloneDX.Spdx.Tests/CycloneDX.Spdx.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj b/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj index e3184a00..1b993b79 100644 --- a/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj +++ b/tests/CycloneDX.Utils.Tests/CycloneDX.Utils.Tests.csproj @@ -7,7 +7,7 @@ - + From b47fd4ec0c99fd2258eb993b1466a2c7f660d739 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 14:56:39 +0200 Subject: [PATCH 007/285] Json/Validator: add findNamedElements() to detect duplicate "bom-ref" definitions Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 04506afe..dd4a5e57 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,6 +166,88 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) + { + if (dict2 == null || dict2.Count == 0) + { + return dict1; + } + + if (dict1 == null || dict1.Count == 0) + { + return dict2; + } + + foreach (KeyValuePair> KVP in dict2) + { + if (dict1.ContainsKey(KVP.Key)) + { + dict1[KVP.Key].AddRange(KVP.Value); + } + else + { + dict1.Add(KVP.Key, KVP.Value); + } + } + + return dict1; + } + + /// + /// Iterate through the JSON document to find JSON objects whose property names + /// match the one we seek, and add such hits to returned list. Recurse and repeat. + /// + /// A JsonElement, starting from JsonDocument.RootElement + /// for the original caller, probably. Then used to recurse. + /// + /// The property name we seek. + /// A Dictionary with distinct values of the seeked JsonElement as keys, + /// and a List of "parent" JsonElement (which contain such key) as mapped values. + /// + private static Dictionary> findNamedElements(JsonElement element, string name) + { + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; + + // Can we iterate further? + switch (element.ValueKind) { + case JsonValueKind.Object: + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.Name == name) { + if (!hits.ContainsKey(property.Value)) + { + hits.Add(property.Value, new List()); + } + hits[property.Value].Add(element); + } + + // Note: Here we can recurse into same property that + // we've just listed, if it is not of a simple kind. + nestedHits = findNamedElements(property.Value, name); + hits = addDictList(hits, nestedHits); + } + break; + + case JsonValueKind.Array: + foreach (JsonElement nestedElem in element.EnumerateArray()) + { + nestedHits = findNamedElements(nestedElem, name); + hits = addDictList(hits, nestedHits); + } + break; + + default: + // No-op for simple types: these values per se have no name + // to learn, and we can not iterate deeper into them. + break; + } + + return hits; + } + private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDocument, string schemaVersionString) { var validationMessages = new List(); @@ -190,6 +272,23 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc } } } + + // The JSON Schema, at least the ones defined by CycloneDX + // and handled by current parser in dotnet ecosystem, can + // not specify or check the uniqueness requirement for the + // "bom-ref" assignments in the overall document (e.g. in + // "metadata/component" and list of "components", as well + // as in "services" and "vulnerabilities", as of CycloneDX + // spec v1.4), so this is checked separately here if the + // document seems structurally intact otherwise. + // Note that this is not a problem for the XML schema with + // its explicit constraint. + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + if (KVP.Value != null && KVP.Value.Count != 1) { + validationMessages.Add($"'bom-ref' value of {KVP.Key.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + } + } } else { From 8893858dc734215991196acff1f4b2ee8dc65591 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 17:44:21 +0200 Subject: [PATCH 008/285] Json/Validator.cs: when checking for (bom-ref) uniqueness, consider string representation of the JsonElement, not object (which is in fact unique for each node) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 39 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index dd4a5e57..681106d7 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,9 +166,9 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } - private static Dictionary> addDictList( - Dictionary> dict1, - Dictionary> dict2) + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) { if (dict2 == null || dict2.Count == 0) { @@ -180,10 +180,12 @@ private static Dictionary> addDictList( return dict2; } - foreach (KeyValuePair> KVP in dict2) + foreach (KeyValuePair> KVP in dict2) { if (dict1.ContainsKey(KVP.Key)) { + // NOTE: Possibly different object, but same string representation! + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -203,25 +205,29 @@ private static Dictionary> addDictList( /// for the original caller, probably. Then used to recurse. /// /// The property name we seek. - /// A Dictionary with distinct values of the seeked JsonElement as keys, - /// and a List of "parent" JsonElement (which contain such key) as mapped values. + /// A Dictionary with distinct values of string representation of the + /// seeked JsonElement as keys, and a List of actual JsonElement objects as + /// mapped values. /// - private static Dictionary> findNamedElements(JsonElement element, string name) + private static Dictionary> findNamedElements(JsonElement element, string name) { - Dictionary> hits = new Dictionary>(); - Dictionary> nestedHits = null; + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; // Can we iterate further? switch (element.ValueKind) { case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - if (!hits.ContainsKey(property.Value)) + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); + string key = property.Value.ToString(); + if (!(hits.ContainsKey(key))) { - hits.Add(property.Value, new List()); + hits.Add(key, new List()); } - hits[property.Value].Add(element); + hits[key].Add(property.Value); } // Note: Here we can recurse into same property that @@ -232,6 +238,7 @@ private static Dictionary> findNamedElements(Json break; case JsonValueKind.Array: + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -283,10 +290,12 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // document seems structurally intact otherwise. // Note that this is not a problem for the XML schema with // its explicit constraint. - Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); - foreach (KeyValuePair> KVP in bomRefs) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { - validationMessages.Add($"'bom-ref' value of {KVP.Key.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } } } From fdd58c712349fd23aba6fd3a38744740c2a5ae9b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 18:09:07 +0200 Subject: [PATCH 009/285] Json/Validator.cs: drop #COMMENTED-DEBUG# for findNamedElements() troubleshooting Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 681106d7..0d2cc76b 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -185,7 +185,6 @@ private static Dictionary> addDictList( if (dict1.ContainsKey(KVP.Key)) { // NOTE: Possibly different object, but same string representation! - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -219,9 +218,7 @@ private static Dictionary> findNamedElements(JsonEleme case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); string key = property.Value.ToString(); if (!(hits.ContainsKey(key))) { @@ -238,7 +235,6 @@ private static Dictionary> findNamedElements(JsonEleme break; case JsonValueKind.Array: - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -290,10 +286,8 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // document seems structurally intact otherwise. // Note that this is not a problem for the XML schema with // its explicit constraint. - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); foreach (KeyValuePair> KVP in bomRefs) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } From f894136d145a175c0ddf4bd3dd6f08cecc6272a2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 22 Jul 2023 02:11:31 +0200 Subject: [PATCH 010/285] Merge.cs, Component.cs: add support for optional mergeWith() method to process the complex object properties to squash two similar items together into one better informed entity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 153 ++++++++++++++++++++++++- src/CycloneDX.Utils/Merge.cs | 50 +++++++- 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 8fa0edba..48ec8ea7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -18,6 +18,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -201,10 +203,159 @@ public bool Equals(Component obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); } - + public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } + + public bool mergeWith(Component obj) + { + if (this.Equals(obj)) + // Contents are identical, nothing to do: + return true; + + if ( + (this.BomRef != null && BomRef.Equals(obj.BomRef)) || + (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) + ) { + // Objects seem equivalent according to critical arguments; + // merge the attribute values with help of reflection: + PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance); + + // Use a temporary clone instead of mangling "this" object right away: + Component tmp = new Component(); + tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + bool mergedOk = true; + + foreach (PropertyInfo property in properties) + { + switch (property.PropertyType) + { + case Type _ when property.PropertyType == typeof(ComponentScope): + { + // Not nullable! + ComponentScope tmpItem = (ComponentScope)property.GetValue(tmp, null); + ComponentScope objItem = (ComponentScope)property.GetValue(obj, null); + if (tmpItem == objItem) + { + continue; + } + else + { + // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) + { + if (objItem != ComponentScope.Excluded) + // keep absent==required; upgrade optional objItem to value of tmp + property.SetValue(tmp, ComponentScope.Required); + continue; + } + + if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + { + if (tmpItem != ComponentScope.Excluded) + // set required; upgrade optional tmpItem (if such) + property.SetValue(tmp, ComponentScope.Required); + continue; + } + } + + // Here throw some exception or trigger creation of new object with a + // new bom-ref - and a new identification in the original document to + // avoid conflicts; be sure then to check for other entries that have + // everything same except bom-ref (match the expected new pattern)?.. + mergedOk = false; + } + break; + + case Type _ when property.PropertyType == typeof(List): + { + foreach (var objItem in ((List)(property.GetValue(obj, null)))) + { + if (objItem is null) + continue; + + bool listHit = false; + foreach (var tmpItem in ((List)(property.GetValue(tmp, null)))) + { + if (tmpItem != null && tmpItem == objItem) + { + listHit = true; + var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + mergedOk = false; + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } // else: no method, just trust equality - avoid "Add" to merge below + else + { + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: no such method"); + } + } + } + + if (!listHit) + { + (((List)property.GetValue(tmp, null))).Add(objItem); + } + } + } + break; + + default: + { + var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + mergedOk = false; + } + catch (System.Exception exc) + { + // That property's class lacks a mergeWith(), gotta trust the equality: + if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + continue; + Console.WriteLine($"FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } + else + { + // That property's class lacks a mergeWith(), gotta trust the equality: + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + continue; + mergedOk = false; + } + } + break; + } + } + + if (mergedOk) { + // No failures, only now update the current object: + foreach (PropertyInfo property in properties) + { + property.SetValue(this, property.GetValue(tmp, null)); + } + } + + return mergedOk; + } + + // Merge was not applicable or otherwise did not succeed + return false; + } } } \ No newline at end of file diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 372649b1..3c717ad3 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,16 +27,58 @@ class ListMergeHelper { public List Merge(List list1, List list2) { + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); if (list1 is null) return list2; if (list2 is null) return list1; - var result = new List(list1); + List result = new List(list1); - foreach (var item in list2) + foreach (var item2 in list2) { - if (!(result.Contains(item))) + bool isContained = false; + for (int i=0; i < result.Count; i++) { - result.Add(item); + T item1 = result[i]; + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there: + var method = item1.GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (((bool)method.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; // items deemed equivalent + } + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } // else: That class lacks a mergeWith(), gotta trust the equality + else + { + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1.Equals(item2)) { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + } + } + + if (!isContained) + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + else + { + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } } From 4d68300526e90fe2e14c30884abc515f8d2aab76 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:34:25 +0200 Subject: [PATCH 011/285] Merge.cs: refactor with GetMethod() called once and using type-specific Equals() implementation, fix remaining duplicates Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 63 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3c717ad3..1ca2b1a9 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -33,25 +33,30 @@ public List Merge(List list1, List list2) List result = new List(list1); + var TType = ((T)list2[0]).GetType(); + var methodMergeWith = TType.GetMethod("mergeWith"); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + foreach (var item2 in list2) { bool isContained = false; + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); for (int i=0; i < result.Count; i++) { + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to // IEquatable<>.Equals() checks defined in respective // classes), if there is a method defined there: - var method = item1.GetType().GetMethod("mergeWith"); - if (method != null) + if (methodMergeWith != null) { try { - if (((bool)method.Invoke(item1, new object[] {item2}))) + if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) { isContained = true; - break; // items deemed equivalent + break; // item2 merged into result[item1] or already equal to it } } catch (System.Exception exc) @@ -61,11 +66,53 @@ public List Merge(List list1, List list2) } // else: That class lacks a mergeWith(), gotta trust the equality else { - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1.Equals(item2)) { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1 is IEquatable) + { + if (methodEquals != null) + { + try + { + Console.WriteLine($"LIST-MERGE: try methodEquals()"); + if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; + } + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } + + if (item1.Equals(item2)) + { + // Fall back to generic equality check which may be useless + Console.WriteLine($"SKIP MERGE: items say they are equal"); + isContained = true; + break; // items deemed equivalent + } + + Console.WriteLine($"MERGE: items say they are not equal"); + } + else + { + Console.WriteLine($"MERGE: items are not IEquatable"); + } +/* + else + { + if (item1 is CycloneDX.Models.Bom) + { + if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) + { + isContained = true; + break; // items deemed equivalent + } + } } +*/ } } From 346dbc631d484c62a8088468b4e707213c03151d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:44:08 +0200 Subject: [PATCH 012/285] Merge.cs: ListMerge: quiesce much of debug message noise Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 1ca2b1a9..f8a2fddd 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -40,10 +40,10 @@ public List Merge(List list1, List list2) foreach (var item2 in list2) { bool isContained = false; - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + /* Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); */ for (int i=0; i < result.Count; i++) { - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + /* Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); */ T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to @@ -66,14 +66,14 @@ public List Merge(List list1, List list2) } // else: That class lacks a mergeWith(), gotta trust the equality else { - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + /* Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); */ if (item1 is IEquatable) { if (methodEquals != null) { try { - Console.WriteLine($"LIST-MERGE: try methodEquals()"); + /* Console.WriteLine($"LIST-MERGE: try methodEquals()"); */ if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) { isContained = true; @@ -82,7 +82,7 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + /* Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ } } From c374620b4223c14badc63317abf9fe698975f71d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:58:11 +0200 Subject: [PATCH 013/285] Component.cs: try using ListMergeHelper Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 48ec8ea7..13266117 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -24,6 +24,8 @@ using System.Xml.Serialization; using ProtoBuf; +using CycloneDX.Utils.ListMergeHelper; + namespace CycloneDX.Models { [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] @@ -271,6 +273,25 @@ public bool mergeWith(Component obj) case Type _ when property.PropertyType == typeof(List): { + var listTmp = ((List)(property.GetValue(tmp, null))); + var listObj = ((List)(property.GetValue(obj, null))); +/* + if (listObj == null || listObj.Count == 0) + { + // Keep whatever "this" version of the list as the only one relevant + break; + } + + if (listTmp == null || listTmp.Count == 0) + { + // Keep whatever "other" version of the list as the only one relevant + property.SetValue(tmp, listObj); + break; + } +*/ + var propertyMerger = new ListMergeHelper(); + property.SetValue(tmp, propertyMerger.Merge(listTmp, listObj)); +/* foreach (var objItem in ((List)(property.GetValue(obj, null)))) { if (objItem is null) @@ -308,6 +329,7 @@ public bool mergeWith(Component obj) (((List)property.GetValue(tmp, null))).Add(objItem); } } +*/ } break; From b6b4d0a41366e8a417c2423234a5c4b7838488ee Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:58:25 +0200 Subject: [PATCH 014/285] Revert "Component.cs: try using ListMergeHelper" This reverts commit 9239be06d819d1778ea415d59cf287711479ed45. Can't easily pull in Utils dependency. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 13266117..48ec8ea7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -24,8 +24,6 @@ using System.Xml.Serialization; using ProtoBuf; -using CycloneDX.Utils.ListMergeHelper; - namespace CycloneDX.Models { [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] @@ -273,25 +271,6 @@ public bool mergeWith(Component obj) case Type _ when property.PropertyType == typeof(List): { - var listTmp = ((List)(property.GetValue(tmp, null))); - var listObj = ((List)(property.GetValue(obj, null))); -/* - if (listObj == null || listObj.Count == 0) - { - // Keep whatever "this" version of the list as the only one relevant - break; - } - - if (listTmp == null || listTmp.Count == 0) - { - // Keep whatever "other" version of the list as the only one relevant - property.SetValue(tmp, listObj); - break; - } -*/ - var propertyMerger = new ListMergeHelper(); - property.SetValue(tmp, propertyMerger.Merge(listTmp, listObj)); -/* foreach (var objItem in ((List)(property.GetValue(obj, null)))) { if (objItem is null) @@ -329,7 +308,6 @@ public bool mergeWith(Component obj) (((List)property.GetValue(tmp, null))).Add(objItem); } } -*/ } break; From 505ee5d5e55227e70260ba8403fd41558f317b70 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 04:30:18 +0200 Subject: [PATCH 015/285] Component.cs: experiment more for mergeWith() to actually act Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 271 +++++++++++++++++++++---- 1 file changed, 234 insertions(+), 37 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 48ec8ea7..04c9fff0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -212,8 +212,10 @@ public override int GetHashCode() public bool mergeWith(Component obj) { if (this.Equals(obj)) - // Contents are identical, nothing to do: + { + Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); return true; + } if ( (this.BomRef != null && BomRef.Equals(obj.BomRef)) || @@ -221,91 +223,200 @@ public bool mergeWith(Component obj) ) { // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: - PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance); + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); - // Use a temporary clone instead of mangling "this" object right away: + // Use a temporary clone instead of mangling "this" object right away; + // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); - tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + foreach (PropertyInfo property in properties) + { + try { + property.SetValue(tmp, property.GetValue(this, null)); + } catch (System.Exception) { + // no-op + } + } + /* tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ bool mergedOk = true; foreach (PropertyInfo property in properties) { + Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { + case Type _ when property.PropertyType == typeof(Nullable): + break; +/* + case Type _ when property.PropertyType == typeof(Nullable[]): + break; +*/ case Type _ when property.PropertyType == typeof(ComponentScope): { // Not nullable! - ComponentScope tmpItem = (ComponentScope)property.GetValue(tmp, null); - ComponentScope objItem = (ComponentScope)property.GetValue(obj, null); - if (tmpItem == objItem) + ComponentScope tmpItem; + try + { + tmpItem = (ComponentScope)property.GetValue(tmp, null); + } + catch (System.Exception) + { + // Unspecified => required per CycloneDX spec v1.4?.. + tmpItem = ComponentScope.Null; + } + + ComponentScope objItem; + try + { + objItem = (ComponentScope)property.GetValue(obj, null); + } + catch (System.Exception) + { + objItem = ComponentScope.Null; + } + + Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + + // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { + // keep absent==required; upgrade optional objItem + property.SetValue(tmp, ComponentScope.Required); + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); continue; } - else + + if ((tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional)) + { + // downgrade optional objItem to excluded + property.SetValue(tmp, ComponentScope.Excluded); + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + continue; + } + + +/* + if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) { - // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" - if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) + if (objItem != ComponentScope.Excluded) { - if (objItem != ComponentScope.Excluded) - // keep absent==required; upgrade optional objItem to value of tmp - property.SetValue(tmp, ComponentScope.Required); - continue; + // keep absent==required; upgrade optional objItem to value of tmp + property.SetValue(tmp, ComponentScope.Required); + continue; } + } - if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + { + if (tmpItem != ComponentScope.Excluded) { - if (tmpItem != ComponentScope.Excluded) - // set required; upgrade optional tmpItem (if such) - property.SetValue(tmp, ComponentScope.Required); - continue; + // set required; upgrade optional tmpItem (if such) + property.SetValue(tmp, ComponentScope.Required); + continue; } } +*/ // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. + Console.WriteLine($"Component.mergeWith(): can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; - case Type _ when property.PropertyType == typeof(List): + case Type _ when (property.Name == "NonNullableModified"): + { + // Not nullable! + bool tmpItem = (bool)property.GetValue(tmp, null); + bool objItem = (bool)property.GetValue(obj, null); + + Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + if (objItem) + property.SetValue(tmp, true); + } + break; + + case Type _ when (property.PropertyType == typeof(List) || property.PropertyType.ToString().StartsWith("System.Collections.Generic.List")): { - foreach (var objItem in ((List)(property.GetValue(obj, null)))) + // https://www.experts-exchange.com/questions/22600200/Traverse-generic-List-using-C-Reflection.html + var propValTmp = property.GetValue(tmp); + var propValObj = property.GetValue(obj); + if (propValTmp == null && propValObj == null) + { + Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + continue; + } + + var LType = (propValTmp == null ? propValObj.GetType() : propValTmp.GetType()); + var propCount = LType.GetProperty("Count"); + var methodGetItem = LType.GetMethod("get_Item"); + var methodAdd = LType.GetMethod("Add"); + if (methodGetItem == null || propCount == null || methodAdd == null) + { + Console.WriteLine($"Component.mergeWith(): is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + mergedOk = false; + continue; + } + + int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); + int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); + Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + + if (propValObj == null || propValObjCount == 0 || propValObjCount == null) { + continue; + } + + if (propValTmp == null || propValTmpCount == 0 || propValTmpCount == null) + { + property.SetValue(tmp, propValObj); + continue; + } + + var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); + var methodMergeWith = TType.GetMethod("mergeWith"); + + for (int o = 0; o < propValObjCount; o++) + { + var objItem = methodGetItem.Invoke(propValObj, new object[] { o }); if (objItem is null) continue; bool listHit = false; - foreach (var tmpItem in ((List)(property.GetValue(tmp, null)))) + for (int t = 0; t < propValTmpCount; t++) { - if (tmpItem != null && tmpItem == objItem) + var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); + if (tmpItem != null) { listHit = true; - var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); - if (method != null) + if (methodMergeWith != null) { try { - if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: no such method"); + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); */ } - } + } // else: tmpitem considered not equal, should be added } if (!listHit) { - (((List)property.GetValue(tmp, null))).Add(objItem); + methodAdd.Invoke(propValTmp, new object[] {objItem}); + propValTmpCount = (int)propCount.GetValue(propValTmp, null); } } } @@ -313,29 +424,110 @@ public bool mergeWith(Component obj) default: { - var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); - if (method != null) + if ( + /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ + property.PropertyType.ToString().StartsWith("System.Nullable") + ) + { + // e.g. 'Scope' helper + // followed by 'Scope' + // which we specially handle above + Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + continue; + } + + Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + var propValTmp = property.GetValue(tmp, null); + var propValObj = property.GetValue(obj, null); + if (propValObj == null) + { + continue; + } + + if (propValTmp == null) + { + property.SetValue(tmp, propValObj); + continue; + } + + var TType = propValTmp.GetType(); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + bool propsSeemEqual = false; + bool propsSeemEqualLearned = false; + + try + { + if (methodEquals != null) + { + /* Console.WriteLine($"Component.mergeWith(): try methodEquals()"); */ + propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + /* Console.WriteLine($"Component.mergeWith(): can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + } + + try + { + if (!propsSeemEqualLearned) + { + // Fall back to generic equality check which may be useless + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + propsSeemEqual = propValTmp.Equals(propValObj); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + } + + try + { + if (!propsSeemEqualLearned) + { + // Fall back to generic equality check which may be useless + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + propsSeemEqual = (propValTmp == propValObj); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + } + + if (!propsSeemEqual) + { + Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + } + + var methodMergeWith = TType.GetMethod("mergeWith"); + if (methodMergeWith != null) { try { - if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + if (!((bool)methodMergeWith.Invoke(propValTmp, new object[] {propValObj}))) mergedOk = false; } catch (System.Exception exc) { // That property's class lacks a mergeWith(), gotta trust the equality: - if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + if (propsSeemEqual) continue; - Console.WriteLine($"FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } else { // That property's class lacks a mergeWith(), gotta trust the equality: - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); - if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + if (propsSeemEqual) continue; + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -351,8 +543,13 @@ public bool mergeWith(Component obj) } } + Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } + else + { + Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + } // Merge was not applicable or otherwise did not succeed return false; From 4228c20f93a5a042eeef80daaebaed3d3bf380ba Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:20:00 +0200 Subject: [PATCH 016/285] Merge.cs, Component.cs: relegate debug trace printing to CYCLONEDX_DEBUG_MERGE envvar Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 81 +++++++++++++++++--------- src/CycloneDX.Utils/Merge.cs | 39 +++++++++---- 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 04c9fff0..3c9373b7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -211,9 +211,13 @@ public override int GetHashCode() public bool mergeWith(Component obj) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + if (this.Equals(obj)) { - Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); return true; } @@ -223,9 +227,11 @@ public bool mergeWith(Component obj) ) { // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases @@ -243,7 +249,8 @@ public bool mergeWith(Component obj) foreach (PropertyInfo property in properties) { - Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); + if (iDebugLevel >= 2) + Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -276,14 +283,16 @@ public bool mergeWith(Component obj) objItem = ComponentScope.Null; } - Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); continue; } @@ -291,7 +300,8 @@ public bool mergeWith(Component obj) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -322,7 +332,8 @@ public bool mergeWith(Component obj) // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. - Console.WriteLine($"Component.mergeWith(): can not merge two bom-refs with scope excluded and required"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; @@ -333,7 +344,8 @@ public bool mergeWith(Component obj) bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); - Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); } @@ -346,7 +358,8 @@ public bool mergeWith(Component obj) var propValObj = property.GetValue(obj); if (propValTmp == null && propValObj == null) { - Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -356,14 +369,16 @@ public bool mergeWith(Component obj) var methodAdd = LType.GetMethod("Add"); if (methodGetItem == null || propCount == null || methodAdd == null) { - Console.WriteLine($"Component.mergeWith(): is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); mergedOk = false; continue; } int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); - Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount == 0 || propValObjCount == null) { @@ -396,19 +411,22 @@ public bool mergeWith(Component obj) { try { - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); */ + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added } @@ -432,11 +450,13 @@ public bool mergeWith(Component obj) // e.g. 'Scope' helper // followed by 'Scope' // which we specially handle above - Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); continue; } - Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -459,7 +479,8 @@ public bool mergeWith(Component obj) { if (methodEquals != null) { - /* Console.WriteLine($"Component.mergeWith(): try methodEquals()"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; } @@ -467,7 +488,8 @@ public bool mergeWith(Component obj) catch (System.Exception exc) { // no-op - /* Console.WriteLine($"Component.mergeWith(): can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } try @@ -475,7 +497,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -490,7 +513,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -502,7 +526,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqual) { - Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): items say they are not equal"); } var methodMergeWith = TType.GetMethod("mergeWith"); @@ -518,7 +543,8 @@ public bool mergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } @@ -527,7 +553,8 @@ public bool mergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -543,12 +570,14 @@ public bool mergeWith(Component obj) } } - Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } else { - Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index f8a2fddd..4728b21e 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,7 +27,11 @@ class ListMergeHelper { public List Merge(List list1, List list2) { - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); if (list1 is null) return list2; if (list2 is null) return list1; @@ -40,10 +44,12 @@ public List Merge(List list1, List list2) foreach (var item2 in list2) { bool isContained = false; - /* Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); */ + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); for (int i=0; i < result.Count; i++) { - /* Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); */ + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to @@ -61,19 +67,22 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 1) + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); } } // else: That class lacks a mergeWith(), gotta trust the equality else { - /* Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); */ + if (iDebugLevel >= 6) + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); if (item1 is IEquatable) { if (methodEquals != null) { try { - /* Console.WriteLine($"LIST-MERGE: try methodEquals()"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: try methodEquals()"); if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) { isContained = true; @@ -82,23 +91,27 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - /* Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); } } if (item1.Equals(item2)) { // Fall back to generic equality check which may be useless - Console.WriteLine($"SKIP MERGE: items say they are equal"); + if (iDebugLevel >= 3) + Console.WriteLine($"SKIP MERGE: items say they are equal"); isContained = true; break; // items deemed equivalent } - Console.WriteLine($"MERGE: items say they are not equal"); + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items say they are not equal"); } else { - Console.WriteLine($"MERGE: items are not IEquatable"); + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items are not IEquatable"); } /* else @@ -120,12 +133,14 @@ public List Merge(List list1, List list2) { // Add new entry "as is" (new-ness is subject to // equality checks of respective classes): - Console.WriteLine($"WILL ADD: {item2.ToString()}"); + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); result.Add(item2); } else { - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } } From 360ac980b7e164728988bd8ae8c1b89c1fe58d45 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:20:45 +0200 Subject: [PATCH 017/285] Component.cs: address some compiler warnings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 3c9373b7..06ff6ad5 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -236,6 +236,9 @@ public bool mergeWith(Component obj) // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); + /* This fails due to copy of "non-null" fields which may be null: + * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + */ foreach (PropertyInfo property in properties) { try { @@ -244,7 +247,6 @@ public bool mergeWith(Component obj) // no-op } } - /* tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ bool mergedOk = true; foreach (PropertyInfo property in properties) @@ -328,6 +330,7 @@ public bool mergeWith(Component obj) } */ + // TODO: Having two same bom-refs is a syntax validation error... // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have @@ -380,12 +383,12 @@ public bool mergeWith(Component obj) if (iDebugLevel >= 4) Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); - if (propValObj == null || propValObjCount == 0 || propValObjCount == null) + if (propValObj == null || propValObjCount < 1) { continue; } - if (propValTmp == null || propValTmpCount == 0 || propValTmpCount == null) + if (propValTmp == null || propValTmpCount < 1) { property.SetValue(tmp, propValObj); continue; @@ -503,7 +506,7 @@ public bool mergeWith(Component obj) propsSeemEqualLearned = true; } } - catch (System.Exception exc) + catch (System.Exception) { // no-op } @@ -519,7 +522,7 @@ public bool mergeWith(Component obj) propsSeemEqualLearned = true; } } - catch (System.Exception exc) + catch (System.Exception) { // no-op } From 7b1ad2b54a6d2e6c3b700c7ecb3015f8953435b1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:31:52 +0200 Subject: [PATCH 018/285] Component.cs: clean up handling of SCOPE merging Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 29 ++++---------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 06ff6ad5..7cb99833 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -298,8 +298,10 @@ public bool mergeWith(Component obj) continue; } - if ((tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional)) - { + if ( + (tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || + (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional) + ) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 3) @@ -307,29 +309,6 @@ public bool mergeWith(Component obj) continue; } - -/* - if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) - { - if (objItem != ComponentScope.Excluded) - { - // keep absent==required; upgrade optional objItem to value of tmp - property.SetValue(tmp, ComponentScope.Required); - continue; - } - } - - if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) - { - if (tmpItem != ComponentScope.Excluded) - { - // set required; upgrade optional tmpItem (if such) - property.SetValue(tmp, ComponentScope.Required); - continue; - } - } -*/ - // TODO: Having two same bom-refs is a syntax validation error... // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to From 09440619a16cc8ed1d908ecabfe89c2dfcee9419 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 13:51:54 +0200 Subject: [PATCH 019/285] Merge.cs: always apply a new timestamp to freshly created "result" Bom object Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 4728b21e..51213fb6 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -166,15 +166,21 @@ public static partial class CycloneDXUtils public static Bom FlatMerge(Bom bom1, Bom bom2) { var result = new Bom(); + result.Metadata = new Metadata + { + // Note: we recurse into this method from other FlatMerge() implementations + // (e.g. mass-merge of a big list of Bom documents), so the resulting + // document gets a new timestamp every time. It is unique after all. + // Also note that a merge of "new Bom()" with a real Bom is also different + // from that original (serialNumber, timestamp, possible entry order, etc.) + Timestamp = DateTime.Now + }; var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { - result.Metadata = new Metadata - { - Tools = tools - }; + result.Metadata.Tools = tools; } var componentsMerger = new ListMergeHelper(); @@ -239,7 +245,11 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); - + + // Note: we were asked to "merge" and so we do, per principle of + // least surprise - even if there is just one entry in boms[] so + // we might be inclined to skip the loop. Resulting document WILL + // differ from such single original (serialNumber, timestamp...) foreach (var bom in boms) { result = FlatMerge(result, bom); @@ -267,8 +277,6 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) } result.Dependencies.Add(mainDependency); - - } return result; @@ -291,14 +299,16 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + result.Metadata = new Metadata + { + Timestamp = DateTime.Now + }; + if (bomSubject != null) { if (bomSubject.BomRef is null) bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); - result.Metadata = new Metadata - { - Component = bomSubject, - Tools = new List(), - }; + result.Metadata.Component = bomSubject; + result.Metadata.Tools = new List(); } result.Components = new List(); From b7e2f32729349c0aa91bad27847bf321311455c1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 13:53:11 +0200 Subject: [PATCH 020/285] Merge.cs: HierarchicalMerge(): be sure to have a non-null result.Metadata.Tools list before adding into it (might be AWOL if bomSubject==null at start) Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 51213fb6..73127990 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -332,6 +332,11 @@ bom.SerialNumber is null if (bom.Metadata?.Tools?.Count > 0) { + if (result.Metadata.Tools == null) + { + result.Metadata.Tools = new List(); + } + result.Metadata.Tools.AddRange(bom.Metadata.Tools); } From 000cb9af3ae0e68bc60166b1069036354d86e9b4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 15:51:38 +0200 Subject: [PATCH 021/285] Merge.cs: add logic to CleanupMetadataComponent() and CleanupEmptyLists() as a finishing touch, to avoid inducing a spec violation with a duplicate bom-ref or publishing empty JSON lists Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 61 +++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 73127990..661acf3f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -399,14 +399,61 @@ bom.SerialNumber is null }); } + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + + return result; + } + + /// + /// Merge main "metadata/component" entry with its possible alter-ego + /// in the components list and evict extra copy from that list: per + /// spec v1_4 at least, the bom-ref must be unique across the document. + /// + /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupMetadataComponent(Bom result) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); + if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) + { + if (iDebugLevel >= 2) + Console.WriteLine($"MERGE-CLEANUP: Searching in list"); + foreach (Component component in result.Components) + { + if (iDebugLevel >= 2) + Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); + if (component is null) continue; // should not happen + if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) + { + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); + result.Metadata.Component.mergeWith(component); + result.Components.Remove(component); + return result; + } + } + } + + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: NO HITS"); + return result; + } + + public static Bom CleanupEmptyLists(Bom result) + { // cleanup empty top level elements - if (result.Metadata.Tools.Count == 0) result.Metadata.Tools = null; - if (result.Components.Count == 0) result.Components = null; - if (result.Services.Count == 0) result.Services = null; - if (result.ExternalReferences.Count == 0) result.ExternalReferences = null; - if (result.Dependencies.Count == 0) result.Dependencies = null; - if (result.Compositions.Count == 0) result.Compositions = null; - if (result.Vulnerabilities.Count == 0) result.Vulnerabilities = null; + if (result.Metadata?.Tools?.Count == 0) result.Metadata.Tools = null; + if (result.Components?.Count == 0) result.Components = null; + if (result.Services?.Count == 0) result.Services = null; + if (result.ExternalReferences?.Count == 0) result.ExternalReferences = null; + if (result.Dependencies?.Count == 0) result.Dependencies = null; + if (result.Compositions?.Count == 0) result.Compositions = null; + if (result.Vulnerabilities?.Count == 0) result.Vulnerabilities = null; return result; } From bafe5a5190a7f641a9dcb2c205908623fb28eb96 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 15:52:42 +0200 Subject: [PATCH 022/285] Merge.cs: be more careful about populating metadata/component vs. components[] array; use CleanupMetadataComponent() and CleanupEmptyLists() as a finishing touch, to avoid inducing a spec violation with a duplicate bom-ref or publishing empty JSON lists Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 53 ++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 661acf3f..bde7f240 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -165,6 +165,9 @@ public static partial class CycloneDXUtils /// public static Bom FlatMerge(Bom bom1, Bom bom2) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + var result = new Bom(); result.Metadata = new Metadata { @@ -186,10 +189,37 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var componentsMerger = new ListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); - //Add main component if missing - if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) + // Add main component from bom2 as a "yet another component" + // if missing in that list so far. Note: any more complicated + // cases should be handled by CleanupMetadataComponent() when + // called by MergeCommand or similar consumer; however we can + // not generally rely in a library that only one particular + // tool calls it - so this method should ensure validity of + // its own output on every step along the way. + if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) { - result.Components.Add(bom2.Metadata.Component); + // Skip such addition if the component in bom2 is same as the + // existing metadata/component in bom1 (gluing same file together + // twice should be effectively no-op); try to merge instead: + + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); + + if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) + || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) + { + // bom1's entry is not null and seems equivalent to bom2's: + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); + result.Metadata.Component = bom1.Metadata.Component; + result.Metadata.Component.mergeWith(bom2.Metadata.Component); + } + else + { + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); + result.Components.Add(bom2.Metadata.Component); + } } var servicesMerger = new ListMergeHelper(); @@ -207,6 +237,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var vulnerabilitiesMerger = new ListMergeHelper(); result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities); + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + return result; } @@ -257,9 +290,14 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) if (bomSubject != null) { - // use the params provided if possible - result.Metadata.Component = bomSubject; - result.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + // use the params provided if possible: prepare a new document + // with desired "metadata/component" and merge differing data + // from earlier collected result into this structure. + var resultSubj = new Bom(); + + resultSubj.Metadata.Component = bomSubject; + resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + result = FlatMerge(resultSubj, result); var mainDependency = new Dependency(); mainDependency.Ref = result.Metadata.Component.BomRef; @@ -279,6 +317,9 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result.Dependencies.Add(mainDependency); } + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + return result; } From 7435b55f77d6e4a3d975cd19f02055142549aa2c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 21:37:41 +0200 Subject: [PATCH 023/285] Components.cs: mergeWith(): revise exception catching for NonNullable types Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 7cb99833..819268aa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -243,8 +243,8 @@ public bool mergeWith(Component obj) { try { property.SetValue(tmp, property.GetValue(this, null)); - } catch (System.Exception) { - // no-op + } catch (System.InvalidOperationException) { + // no-op, skip factually null values of NonNullable types } } bool mergedOk = true; @@ -269,7 +269,7 @@ public bool mergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { // Unspecified => required per CycloneDX spec v1.4?.. tmpItem = ComponentScope.Null; @@ -280,7 +280,7 @@ public bool mergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { objItem = ComponentScope.Null; } From f47440079247878c57a434700a1594a5c324f59a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 21:38:06 +0200 Subject: [PATCH 024/285] Components.cs: mergeWith(): fix equality check for list items Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 64 ++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 819268aa..8692999e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -375,6 +375,7 @@ public bool mergeWith(Component obj) var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); var methodMergeWith = TType.GetMethod("mergeWith"); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); for (int o = 0; o < propValObjCount; o++) { @@ -388,29 +389,56 @@ public bool mergeWith(Component obj) var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); if (tmpItem != null) { - listHit = true; - if (methodMergeWith != null) + // EQ CHECK + bool propsSeemEqual = false; + bool propsSeemEqualLearned = false; + + try { - try - { - if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); - if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) - mergedOk = false; - } - catch (System.Exception exc) + if (methodEquals != null) { - if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); - mergedOk = false; + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); + propsSeemEqualLearned = true; } - } // else: no method, just trust equality - avoid "Add" to merge below - else + } + catch (System.Exception exc) { - if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + // no-op + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } - } // else: tmpitem considered not equal, should be added + + if (propsSeemEqual || !propsSeemEqualLearned) + { + // Got an equivalently-looking item on both sides! + // If there is no mergeWith() in its class, consider + // the two entries just equal (no-op to merge them). + listHit = true; + if (methodMergeWith != null) + { + try + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) + mergedOk = false; + } + catch (System.Exception exc) + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } // else: no method, just trust equality - avoid "Add" to merge below + else + { + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + } + } // else: tmpitem considered not equal, should be added + } } if (!listHit) From 545039058c998ab609eac419492896dc8270b0ec Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 6 Aug 2023 02:10:11 +0200 Subject: [PATCH 025/285] Revert "Components.cs: mergeWith(): revise exception catching for NonNullable types" This reverts commit 9ead840bef0353b24ee26995e981ed39829b35e2. Seems to be involved in more un-deduped entries than without it. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 8692999e..c6b87a0a 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -243,8 +243,8 @@ public bool mergeWith(Component obj) { try { property.SetValue(tmp, property.GetValue(this, null)); - } catch (System.InvalidOperationException) { - // no-op, skip factually null values of NonNullable types + } catch (System.Exception) { + // no-op } } bool mergedOk = true; @@ -269,7 +269,7 @@ public bool mergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.InvalidOperationException) + catch (System.Exception) { // Unspecified => required per CycloneDX spec v1.4?.. tmpItem = ComponentScope.Null; @@ -280,7 +280,7 @@ public bool mergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.InvalidOperationException) + catch (System.Exception) { objItem = ComponentScope.Null; } From 0072e9c35bb07cc8927cae3fcf81d950527430e9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 13:34:25 +0200 Subject: [PATCH 026/285] Introduce a CycloneDX.Core/Models/BomEntity.cs as a base class for shared features Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 6 + src/CycloneDX.Core/Models/BomEntity.cs | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/CycloneDX.Core/Models/BomEntity.cs diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index ecaf4380..2348fc9d 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -58,6 +58,12 @@ public static string Serialize(Bom bom) return jsonBom; } + internal static string Serialize(BomEntity entity) + { + Contract.Requires(entity != null); + return JsonSerializer.Serialize(entity, _options); + } + internal static string Serialize(Component component) { Contract.Requires(component != null); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs new file mode 100644 index 00000000..4a1c8fde --- /dev/null +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -0,0 +1,144 @@ +// This file is part of CycloneDX Library for .NET +// +// 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. +// +// 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.Linq; +using System.Text.Json.Serialization; + +namespace CycloneDX.Models +{ + [Serializable] + class BomEntityConflictException : Exception + { + public BomEntityConflictException() + : base(String.Format("Unresolvable conflict in Bom entities")) + { } + + public BomEntityConflictException(Type type) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type.ToString())) + { } + + public BomEntityConflictException(string msg) + : base(String.Format("Unresolvable conflict in Bom entities: {0}", msg)) + { } + + public BomEntityConflictException(string msg, Type type) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) + { } + } + [Serializable] + class BomEntityIncompatibleException : Exception + { + public BomEntityIncompatibleException() + : base(String.Format("Comparing incompatible Bom entities")) + { } + + public BomEntityIncompatibleException(Type type1, Type type2) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1.ToString(), type2.ToString())) + { } + + public BomEntityIncompatibleException(string msg) + : base(String.Format("Comparing incompatible Bom entities: {0}", msg)) + { } + + public BomEntityIncompatibleException(string msg, Type type1, Type type2) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1.ToString(), type2.ToString(), msg)) + { } + } + + /// + /// BomEntity is intended as a base class for other classes in CycloneDX.Models, + /// which in turn encapsulate different concepts and data types described by + /// the specification. It allows them to share certain behaviors such as the + /// ability to determine "equivalent but not equal" objects (e.g. two instances + /// of a Component with the same "bom-ref" but different in some properties), + /// and to define the logic for merge-ability of such objects while coding much + /// of the logical scaffolding only once. + /// + public class BomEntity : IEquatable + { + protected BomEntity() + { + // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); + } + + public bool Equals(BomEntity other) + { + if (other is null || this.GetType() != other.GetType()) return false; + return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(other); + } + + public override int GetHashCode() + { + return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + } + + /// + /// Do this and other objects describe the same real-life entity? + /// Override this in sub-classes that have a more detailed definition of + /// equivalence (e.g. that certain fields are equal even if whole contents + /// are not). + /// + /// Another object of same type + /// True if two data objects are considered to represent + /// the same real-life entity, False otherwise. + public bool Equivalent(BomEntity other) + { + return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); + } + + /// + /// Default implementation just "agrees" that Equals()==true objects + /// are already merged (returns true), and that Equivalent()==false + /// objects are not (returns false), and for others (equivalent but + /// not equal, or different types) raises an exception. + /// Treats a null "other" object as a success (it is effectively a + /// no-op merge, which keeps "this" object as is). + /// + /// Another object of same type whose additional + /// non-conflicting data we try to squash into this object. + /// True if merge was successful, False if it these objects + /// are not equivalent, or throws if merge can not be done (including + /// lack of merge logic or unresolvable conflicts in data points). + /// + /// Source data problem: two entities with conflicting information + /// Caller error: somehow merging different entity types + public bool MergeWith(BomEntity other) + { + if (other is null) return true; + if (this.GetType() != other.GetType()) + { + // Note: potentially descendent classes can catch this + // to adapt their behavior... if some two different + // classes would ever describe something comparable + // in real life. + throw new BomEntityIncompatibleException(this.GetType(), other.GetType()); + } + + if (this.Equals(other)) return true; + if (!this.Equivalent(other)) return false; + + // Normal mode of operation: descendant classes catch this + // exception to use their custom non-trivial merging logic. + throw new BomEntityConflictException( + "Base-method implementation treats equivalent but not equal entities as conflicting", + this.GetType()); + } + } +} From 36981c1a244aa33bad77b2d1acb0daf19e1299e2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 13:37:14 +0200 Subject: [PATCH 027/285] Hash.cs: implement as a BomEntity descendant class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Hash.cs | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Hash.cs b/src/CycloneDX.Core/Models/Hash.cs index 567f8446..4f54fa6f 100644 --- a/src/CycloneDX.Core/Models/Hash.cs +++ b/src/CycloneDX.Core/Models/Hash.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [XmlType("hash")] [ProtoContract] - public class Hash + public class Hash : BomEntity { [ProtoContract] public enum HashAlgorithm @@ -62,5 +62,40 @@ public enum HashAlgorithm [XmlText] [ProtoMember(2)] public string Content { get; set; } + + public bool Equivalent(Hash other) + { + return (!(other is null) && this.Alg == other.Alg); + } + + public bool MergeWith(Hash other) + { + try + { + // Basic checks for null, type compatibility, + // equality and non-equivalence; throws for + // the hard stuff to implement in the catch: + return base.MergeWith(other); + } + catch (BomEntityConflictException) + { + // Note: Alg is non-nullable so no check for that + if (this.Content is null && !(other.Content is null)) + { + this.Content = other.Content; + return true; + } + + if (this.Content != other.Content) + { + throw new BomEntityConflictException("Two Hash objects with same Alg='${this.Alg}' and different Content: '${this.Content}' vs. '${other.Content}'"); + } + + // All known properties merged or were equal/equivalent + return true; + } + + // Should not get here + } } } \ No newline at end of file From bf86c26fac999cfc3e1e0e7cb112f2b01aa78b5f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:15:08 +0200 Subject: [PATCH 028/285] Restore simplistic CycloneDX.Utils/Merge.cs logic for ListMergeHelper: move the BomEntity related complexity to CycloneDX.Core/BomUtils.cs (initially "as is") Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 122 +++++++++++++++++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 120 +++----------------------------- 2 files changed, 133 insertions(+), 109 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 4e522578..50f97ae2 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -238,5 +238,127 @@ public static void EnumerateAllServices(Bom bom, Action callback) } } } + + public static List MergeBomEntityLists(List list1, List list2) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); + if (list1 is null) return list2; + if (list2 is null) return list1; + + List result = new List(list1); + + var TType = ((T)list2[0]).GetType(); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + + foreach (var item2 in list2) + { + bool isContained = false; + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + for (int i=0; i < result.Count; i++) + { + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + T item1 = result[i]; + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there: + if (methodMergeWith != null) + { + try + { + if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + } + catch (System.Exception exc) + { + if (iDebugLevel >= 1) + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } // else: That class lacks a mergeWith(), gotta trust the equality + else + { + if (iDebugLevel >= 6) + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1 is IEquatable) + { + if (methodEquals != null) + { + try + { + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: try methodEquals()"); + if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; + } + } + catch (System.Exception exc) + { + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } + + if (item1.Equals(item2)) + { + // Fall back to generic equality check which may be useless + if (iDebugLevel >= 3) + Console.WriteLine($"SKIP MERGE: items say they are equal"); + isContained = true; + break; // items deemed equivalent + } + + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items say they are not equal"); + } + else + { + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items are not IEquatable"); + } +/* + else + { + if (item1 is CycloneDX.Models.Bom) + { + if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) + { + isContained = true; + break; // items deemed equivalent + } + } + } +*/ + } + } + + if (!isContained) + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + else + { + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } + } + + return result; + } } } \ No newline at end of file diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index bde7f240..ce6fc20e 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,120 +27,22 @@ class ListMergeHelper { public List Merge(List list1, List list2) { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); - if (list1 is null) return list2; - if (list2 is null) return list1; - - List result = new List(list1); + if (((T)list2[0]).GetType() is BomEntity) + { + return BomUtils.MergeBomEntityLists(list1, list2); + } - var TType = ((T)list2[0]).GetType(); - var methodMergeWith = TType.GetMethod("mergeWith"); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + // Lists of legacy types + var result = new List(list1); - foreach (var item2 in list2) + foreach (var item in list2) { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); - for (int i=0; i < result.Count; i++) + if (!(result.Contains(item))) { - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - T item1 = result[i]; - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there: - if (methodMergeWith != null) - { - try - { - if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 1) - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } // else: That class lacks a mergeWith(), gotta trust the equality - else - { - if (iDebugLevel >= 6) - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1 is IEquatable) - { - if (methodEquals != null) - { - try - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: try methodEquals()"); - if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } - - if (item1.Equals(item2)) - { - // Fall back to generic equality check which may be useless - if (iDebugLevel >= 3) - Console.WriteLine($"SKIP MERGE: items say they are equal"); - isContained = true; - break; // items deemed equivalent - } - - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items say they are not equal"); - } - else - { - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items are not IEquatable"); - } -/* - else - { - if (item1 is CycloneDX.Models.Bom) - { - if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) - { - isContained = true; - break; // items deemed equivalent - } - } - } -*/ - } - } - - if (!isContained) - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } - else - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + result.Add(item); } } From 62bf863e4aae08a18a96e6cf35b7921775b0995b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:41:37 +0200 Subject: [PATCH 029/285] Restore simplistic CycloneDX.Utils/Merge.cs logic for ListMergeHelper: move the BomEntity related complexity to CycloneDX.Core/BomUtils.cs (refactor for BomEntity) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 136 ++++++++++++--------------------- src/CycloneDX.Utils/Merge.cs | 15 +++- 2 files changed, 62 insertions(+), 89 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 50f97ae2..51f2a5a4 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -244,121 +244,83 @@ public static List MergeBomEntityLists(List list1, List= 1) - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); - if (list1 is null) return list2; - if (list2 is null) return list1; + Console.WriteLine($"List-Merge for: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - List result = new List(list1); + // Check actual subtypes of list entries + // TODO: Reflection to get generic List<> type argument? + // This would avoid lists of mixed BomEntity descendant objects + // typed truly as a List by caller... + Type TType = list1[0].GetType(); + Type TType2 = list2[0].GetType(); + if (TType == typeof(BomEntity) || TType2 == typeof(BomEntity)) + { + // Should not happen, but... + throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types (one of these seems to be the base class)", TType, TType2); + } + if (TType != TType2) + { + throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types", TType, TType2); + } - var TType = ((T)list2[0]).GetType(); - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listType = typeof(List<>); + var constructedListType = listType.MakeGenericType(TType); + List result = (List)Activator.CreateInstance(constructedListType); + result.AddRange(list1); foreach (var item2 in list2) { bool isContained = false; if (iDebugLevel >= 3) Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + for (int i=0; i < result.Count; i++) { if (iDebugLevel >= 3) Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - T item1 = result[i]; + var item1 = result[i]; + // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there: - if (methodMergeWith != null) + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + if (item1.MergeWith(item2)) { - try - { - if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 1) - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } // else: That class lacks a mergeWith(), gotta trust the equality - else - { - if (iDebugLevel >= 6) - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1 is IEquatable) - { - if (methodEquals != null) - { - try - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: try methodEquals()"); - if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } - - if (item1.Equals(item2)) - { - // Fall back to generic equality check which may be useless - if (iDebugLevel >= 3) - Console.WriteLine($"SKIP MERGE: items say they are equal"); - isContained = true; - break; // items deemed equivalent - } - - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items say they are not equal"); - } - else - { - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items are not IEquatable"); - } -/* - else - { - if (item1 is CycloneDX.Models.Bom) - { - if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) - { - isContained = true; - break; // items deemed equivalent - } - } - } -*/ + isContained = true; + break; // item2 merged into result[item1] or already equal to it } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. } - if (!isContained) + if (isContained) { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } else { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); } } return result; - } + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index ce6fc20e..3c4548e1 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -30,9 +30,9 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (((T)list2[0]).GetType() is BomEntity) + if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - return BomUtils.MergeBomEntityLists(list1, list2); + return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } // Lists of legacy types @@ -52,6 +52,17 @@ public List Merge(List list1, List list2) public static partial class CycloneDXUtils { + // TOTHINK: Now that we have a BomEntity base class, shouldn't + // this logic relocate to become a Bom.MergeWith() implementation? + // Notably, sanity checks like CleanupMetadataComponent and making + // sure that a Bom+Bom merge produces a spec-validatable result + // should be a concern of that class (same as we coerce other + // classes to perform a structure-dependent meaningful merge, + // and same as the types in its source code handle non-nullable + // properties, etc.) - right?.. Perhaps sub-classes like BomFlat + // and BomHierarchical and their respective MergeWith() methods + // could be a way forward for this... + /// /// Performs a flat merge of two BOMs. /// From d4dc76f76e6a22c27ce661c37886f266a8648013 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:56:06 +0200 Subject: [PATCH 030/285] Component.cs: implement as a BomEntity descendant class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 69 ++++++++++++++------------ src/CycloneDX.Utils/Merge.cs | 4 +- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c6b87a0a..0150e7fa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -29,7 +29,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("component")] [ProtoContract] - public class Component: IEquatable + public class Component: BomEntity { [ProtoContract] public enum Classification @@ -209,7 +209,12 @@ public override int GetHashCode() return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } - public bool mergeWith(Component obj) + public bool Equivalent(Component obj) + { + return (!(obj is null) && this.BomRef == obj.BomRef); + } + + public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -217,7 +222,7 @@ public bool mergeWith(Component obj) if (this.Equals(obj)) { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); + Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); return true; } @@ -228,10 +233,10 @@ public bool mergeWith(Component obj) // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases @@ -252,7 +257,7 @@ public bool mergeWith(Component obj) foreach (PropertyInfo property in properties) { if (iDebugLevel >= 2) - Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); + Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -286,7 +291,7 @@ public bool mergeWith(Component obj) } if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) @@ -294,7 +299,7 @@ public bool mergeWith(Component obj) // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } @@ -305,7 +310,7 @@ public bool mergeWith(Component obj) // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -315,7 +320,7 @@ public bool mergeWith(Component obj) // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); + Console.WriteLine($"Component.MergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; @@ -327,7 +332,7 @@ public bool mergeWith(Component obj) bool objItem = (bool)property.GetValue(obj, null); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); } @@ -341,7 +346,7 @@ public bool mergeWith(Component obj) if (propValTmp == null && propValObj == null) { if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -352,7 +357,7 @@ public bool mergeWith(Component obj) if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + Console.WriteLine($"Component.MergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); mergedOk = false; continue; } @@ -360,7 +365,7 @@ public bool mergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount < 1) { @@ -374,7 +379,7 @@ public bool mergeWith(Component obj) } var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); - var methodMergeWith = TType.GetMethod("mergeWith"); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); for (int o = 0; o < propValObjCount; o++) @@ -398,7 +403,7 @@ public bool mergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); propsSeemEqualLearned = true; } @@ -407,7 +412,7 @@ public bool mergeWith(Component obj) { // no-op if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } if (propsSeemEqual || !propsSeemEqualLearned) @@ -421,21 +426,21 @@ public bool mergeWith(Component obj) try { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added } @@ -461,12 +466,12 @@ public bool mergeWith(Component obj) // followed by 'Scope' // which we specially handle above if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); continue; } if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -490,7 +495,7 @@ public bool mergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; } @@ -499,7 +504,7 @@ public bool mergeWith(Component obj) { // no-op if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } try @@ -508,7 +513,7 @@ public bool mergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): MIGHT SKIP MERGE: items say they are equal"); + Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -524,7 +529,7 @@ public bool mergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -537,10 +542,10 @@ public bool mergeWith(Component obj) if (!propsSeemEqual) { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } - var methodMergeWith = TType.GetMethod("mergeWith"); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); if (methodMergeWith != null) { try @@ -554,7 +559,7 @@ public bool mergeWith(Component obj) if (propsSeemEqual) continue; if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } @@ -564,7 +569,7 @@ public bool mergeWith(Component obj) if (propsSeemEqual) continue; if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -581,13 +586,13 @@ public bool mergeWith(Component obj) } if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + Console.WriteLine($"Component.MergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } else { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3c4548e1..9eb1a8bc 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -125,7 +125,7 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) if (iDebugLevel >= 1) Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); result.Metadata.Component = bom1.Metadata.Component; - result.Metadata.Component.mergeWith(bom2.Metadata.Component); + result.Metadata.Component.MergeWith(bom2.Metadata.Component); } else { @@ -386,7 +386,7 @@ public static Bom CleanupMetadataComponent(Bom result) { if (iDebugLevel >= 1) Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); - result.Metadata.Component.mergeWith(component); + result.Metadata.Component.MergeWith(component); result.Components.Remove(component); return result; } From 05ad4c6f411498154d889bb7c82bfd64286ba76a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 15:05:40 +0200 Subject: [PATCH 031/285] CycloneDX.Utils/Merge.cs: debug trace if proceeding with legacy logic Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 9eb1a8bc..7ba766bd 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,6 +27,9 @@ class ListMergeHelper { public List Merge(List list1, List list2) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; @@ -35,7 +38,9 @@ public List Merge(List list1, List list2) return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } - // Lists of legacy types + // Lists of legacy types + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); var result = new List(list1); foreach (var item in list2) From 48c3a139586ab7607123eec00647f2c3514ed82f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:40:56 +0200 Subject: [PATCH 032/285] BomUtils.cs: MergeBomEntityLists(): refactor so it builds Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 51f2a5a4..84698909 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -17,6 +17,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Collections; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -266,12 +268,21 @@ public static List MergeBomEntityLists(List list1, List "result" at run-time: Type listType = typeof(List<>); var constructedListType = listType.MakeGenericType(TType); - List result = (List)Activator.CreateInstance(constructedListType); - result.AddRange(list1); + //IList result = (IList)Activator.CreateInstance(constructedListType); //.Cast().ToList(); + var result = Activator.CreateInstance(constructedListType); + + foreach (var item1 in list1) + { + result.Add(item1); + } +*/ + + List result = new List(list1); foreach (var item2 in list2) { @@ -320,7 +331,7 @@ public static List MergeBomEntityLists(List list1, List)result; } } } From ec1a88cbad70149ff5479511216217a311b6ad19 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:43:09 +0200 Subject: [PATCH 033/285] BomEntity: make exception classes public Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 4a1c8fde..5d60dad4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [Serializable] - class BomEntityConflictException : Exception + public class BomEntityConflictException : Exception { public BomEntityConflictException() : base(String.Format("Unresolvable conflict in Bom entities")) @@ -42,8 +42,9 @@ public BomEntityConflictException(string msg, Type type) : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) { } } + [Serializable] - class BomEntityIncompatibleException : Exception + public class BomEntityIncompatibleException : Exception { public BomEntityIncompatibleException() : base(String.Format("Comparing incompatible Bom entities")) From 9fa79ff0a2ab66fdba644e7ef471ce2f2805288b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:45:16 +0200 Subject: [PATCH 034/285] Merge.cs: update comment Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 7ba766bd..21c9daeb 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -38,7 +38,7 @@ public List Merge(List list1, List list2) return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } - // Lists of legacy types + // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); var result = new List(list1); From ef4fc433f0c049606553e5e5d51b117217ec4d8d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:46:03 +0200 Subject: [PATCH 035/285] Tool.cs: subclass from BomEntity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Tool.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index f6a6d145..60353ef7 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Tool: IEquatable + public class Tool: BomEntity { [XmlElement("vendor")] [ProtoMember(1)] @@ -49,12 +49,14 @@ public class Tool: IEquatable public bool Equals(Tool obj) { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); + /*return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj);*/ + return base.Equals(obj); } public override int GetHashCode() { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + /*return CycloneDX.Json.Serializer.Serialize(this).GetHashCode();*/ + return base.GetHashCode(); } } -} \ No newline at end of file +} From 5f0a23d99eff1d65f4e232b73d5f4657ae5d104c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:47:30 +0200 Subject: [PATCH 036/285] Move BomUtils:MergeBomEntityLists() to BomEntityListMergeHelper class for consistency Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 95 -------------------------- src/CycloneDX.Core/Models/BomEntity.cs | 68 ++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 6 +- 3 files changed, 72 insertions(+), 97 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 84698909..16177edb 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -17,8 +17,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Collections; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -240,98 +238,5 @@ public static void EnumerateAllServices(Bom bom, Action callback) } } } - - public static List MergeBomEntityLists(List list1, List list2) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; - - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; - - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - - // Check actual subtypes of list entries - // TODO: Reflection to get generic List<> type argument? - // This would avoid lists of mixed BomEntity descendant objects - // typed truly as a List by caller... - Type TType = list1[0].GetType(); - Type TType2 = list2[0].GetType(); - if (TType == typeof(BomEntity) || TType2 == typeof(BomEntity)) - { - // Should not happen, but... - throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types (one of these seems to be the base class)", TType, TType2); - } - if (TType != TType2) - { - throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types", TType, TType2); - } - -/* - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listType = typeof(List<>); - var constructedListType = listType.MakeGenericType(TType); - //IList result = (IList)Activator.CreateInstance(constructedListType); //.Cast().ToList(); - var result = Activator.CreateInstance(constructedListType); - - foreach (var item1 in list1) - { - result.Add(item1); - } -*/ - - List result = new List(list1); - - foreach (var item2 in list2) - { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); - - for (int i=0; i < result.Count; i++) - { - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - var item1 = result[i]; - - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there. - // For BomEntity descendant instances we assume that - // they have Equals(), Equivalent() and MergeWith() - // methods defined or inherited as is suitable for - // the particular entity type, hence much less code - // and error-checking than there was in the PoC: - if (item1.MergeWith(item2)) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - // MergeWith() may throw BomEntityConflictException which we - // want to propagate to users - their input data is confusing. - // Probably should not throw BomEntityIncompatibleException - // unless the lists truly are of mixed types. - } - - if (isContained) - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); - } - else - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } - } - - return (List)result; - } } } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 5d60dad4..26fb82b7 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -63,6 +63,74 @@ public BomEntityIncompatibleException(string msg, Type type1, Type type2) { } } + public class BomEntityListMergeHelper where T : BomEntity + { + public List Merge(List list1, List list2) + { + //return BomUtils.MergeBomEntityLists(list1, list2); + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for BomEntity derivatives: {list1.GetType().ToString()}"); + + List result = new List(list1); + Type TType = list1[0].GetType(); + + foreach (var item2 in list2) + { + bool isContained = false; + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + + for (int i=0; i < result.Count; i++) + { + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + var item1 = result[i]; + + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + if (item1.MergeWith(item2)) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. + } + + if (isContained) + { + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } + else + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + } + + return result; + } + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 21c9daeb..e78e7260 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -97,14 +97,16 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) Timestamp = DateTime.Now }; - var toolsMerger = new ListMergeHelper(); + var toolsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); + //var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); + //var tools = BomUtils.MergeBomEntityLists(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { result.Metadata.Tools = tools; } - var componentsMerger = new ListMergeHelper(); + var componentsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); // Add main component from bom2 as a "yet another component" From 17a8b3a223eb6f99bf183437aa225debf1765235 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 22:47:33 +0200 Subject: [PATCH 037/285] Merge.cs: unite back uses of BomEntityListMergeHelper and legacy ListMergeHelper so consumers/callers do not have to change Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index e78e7260..d637f394 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -35,7 +35,23 @@ public List Merge(List list1, List list2) if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } } // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) @@ -97,16 +113,14 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) Timestamp = DateTime.Now }; - var toolsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); - //var toolsMerger = new ListMergeHelper(); + var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); - //var tools = BomUtils.MergeBomEntityLists(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { result.Metadata.Tools = tools; } - var componentsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); + var componentsMerger = new ListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); // Add main component from bom2 as a "yet another component" From f7a6ead375fc79bb6fd87b4958b005c080447071 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 08:59:14 +0200 Subject: [PATCH 038/285] BomEntity.cs: refactor with SerializeEntity() helper Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 26fb82b7..789cdc2d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -147,15 +147,25 @@ protected BomEntity() // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); } + /// + /// Helper for comparisons and getting object hash code. + /// Calls our standard CycloneDX.Json.Serializer to use + /// its common options in particular. + /// + internal string SerializeEntity() + { + return CycloneDX.Json.Serializer.Serialize(this); + } + public bool Equals(BomEntity other) { if (other is null || this.GetType() != other.GetType()) return false; - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(other); + return this.SerializeEntity() == other.SerializeEntity(); } public override int GetHashCode() { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + return this.SerializeEntity().GetHashCode(); } /// From 87a2e6ec57f66a39fed5678d3ae5313fe504505d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 11:41:02 +0200 Subject: [PATCH 039/285] BomEntity.cs: fix SerializeEntity() to find custom serializer method if defined for the type Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 789cdc2d..569e1011 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -154,7 +154,21 @@ protected BomEntity() /// internal string SerializeEntity() { - return CycloneDX.Json.Serializer.Serialize(this); + // Do we have a custom serializer defined? Use it! + // (One for BomEntity tends to serialize this base class + // so comes up empty, or has to jump through hoops...) + var myClassType = typeof(CycloneDX.Json.Serializer); + var methodSerializeThis = myClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { this.GetType() }); + if (methodSerializeThis != null) + { + var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); + return res1; + } + + var res = CycloneDX.Json.Serializer.Serialize(this); + return res; } public bool Equals(BomEntity other) From 3412c48064c56084a5d1e02cd04dad6bb513076c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 14:26:25 +0200 Subject: [PATCH 040/285] BomEntity.cs: pre-cache info about custom serializer method if defined for the type (avoid looping main logic with many reflection queries) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 52 +++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 569e1011..6f91b78b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; namespace CycloneDX.Models @@ -142,6 +143,51 @@ public List Merge(List list1, List list2) /// public class BomEntity : IEquatable { + // Keep this info initialized once to cut down on overheads of reflection + // when running in our run-time loops. + // Thanks to https://stackoverflow.com/a/45896403/4715872 for the Func'y trick + // and https://stackoverflow.com/questions/857705/get-all-derived-types-of-a-type + // TOTHINK: Should these be exposed as public or hidden even more strictly? + // Perhaps add getters for a copy? + + /// + /// List of classes derived from BomEntity, prepared startically at start time. + /// + static List KnownEntityTypes = + new Func>(() => + { + List derived_types = new List(); + foreach (var domain_assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var assembly_types = domain_assembly.GetTypes() + .Where(type => type.IsSubclassOf(typeof(BomEntity)) && !type.IsAbstract); + + derived_types.AddRange(assembly_types); + } + return derived_types; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() + /// implementations (if present), prepared startically at start time. + /// + static Dictionary KnownTypeSerializers = + new Func>(() => + { + var jserClassType = typeof(CycloneDX.Json.Serializer); + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = jserClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + protected BomEntity() { // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); @@ -157,11 +203,7 @@ internal string SerializeEntity() // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class // so comes up empty, or has to jump through hoops...) - var myClassType = typeof(CycloneDX.Json.Serializer); - var methodSerializeThis = myClassType.GetMethod("Serialize", - BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, - new Type[] { this.GetType() }); - if (methodSerializeThis != null) + if (KnownTypeSerializers.TryGetValue(this.GetType(), out var methodSerializeThis)) { var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); return res1; From a9a18bcc8c6b382db56555f58f7e4d61b807e3c6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 14:27:29 +0200 Subject: [PATCH 041/285] BomEntity.cs: pre-cache info about Equals(), Equivalent() and MergeWith() overrides in derived BomEntity classes (avoid looking for this info in a loop) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 72 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 6f91b78b..0e5cdaf7 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -188,6 +188,66 @@ public class BomEntity : IEquatable return dict; }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equals() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equivalent() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeEquivalent = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom MergeWith() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeMergeWith = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("MergeWith", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + protected BomEntity() { // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); @@ -267,7 +327,17 @@ public bool MergeWith(BomEntity other) } if (this.Equals(other)) return true; - if (!this.Equivalent(other)) return false; + // Avoid calling Equals => serializer twice for no gain + // (default equivalence is equality): + if (KnownTypeEquivalent.TryGetValue(this.GetType(), out var methodEquivalent)) + { + if (!this.Equivalent(other)) return false; + // else fall through to exception below + } + else + { + return false; // known not equal => not equivalent by default => false + } // Normal mode of operation: descendant classes catch this // exception to use their custom non-trivial merging logic. From 4150cd1aea5552a3618d36ddaef0936c1fc175c1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 16:54:50 +0200 Subject: [PATCH 042/285] Merge.cs: ListMergeHelper: use idiomatic and more efficient typeof(T) rather than list1[0].GetType() Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index d637f394..71fc47b8 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -38,7 +38,7 @@ public List Merge(List list1, List list2) // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); var helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); From fb1b0d0a9b60d7a7b47dc017b972ae93dc25d28d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:14:01 +0200 Subject: [PATCH 043/285] BomEntity: forward from default Equals() and Equivalent() implems to class-customized ones if present Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0e5cdaf7..eaa59c89 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -273,8 +273,23 @@ internal string SerializeEntity() return res; } + /// + /// NOTE: Class methods do not "override" this one because they compare to their type + /// and not to the base BomEntity type objects. They should also not call this method + /// to avoid looping - implement everything needed there directly, if ever needed! + /// Keep in mind that the base implementation calls the SerializeEntity() method which + /// should be by default aware and capable of ultimately serializing the properties + /// relevant to each derived class. + /// + /// Another BomEntity-derived object of same type + /// True if two objects are deemed equal public bool Equals(BomEntity other) { + if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquals)) + { + return (bool)methodEquals.Invoke(this, new object[] {other}); + } + if (other is null || this.GetType() != other.GetType()) return false; return this.SerializeEntity() == other.SerializeEntity(); } @@ -286,15 +301,21 @@ public override int GetHashCode() /// /// Do this and other objects describe the same real-life entity? - /// Override this in sub-classes that have a more detailed definition of + /// "Override" this in sub-classes that have a more detailed definition of /// equivalence (e.g. that certain fields are equal even if whole contents - /// are not). + /// are not) by defining an implementation tailored to that derived type + /// as the argument, or keep this default where equiality is equivalence. /// /// Another object of same type /// True if two data objects are considered to represent /// the same real-life entity, False otherwise. public bool Equivalent(BomEntity other) { + if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquivalent)) + { + return (bool)methodEquivalent.Invoke(this, new object[] {other}); + } + return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); } From bca0d259608d4f8441938aef1c1e73893e6c24ce Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:14:18 +0200 Subject: [PATCH 044/285] Move ListMergeHelper from Merge.cs to CycloneDX.Core so it can be shared by different codebase more efficiently Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 81 +++++++++++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 49 +--------------- 2 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 src/CycloneDX.Core/ListMergeHelper.cs diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs new file mode 100644 index 00000000..e9ab7adb --- /dev/null +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -0,0 +1,81 @@ +// This file is part of CycloneDX Library for .NET +// +// 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. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using CycloneDX.Models; + +namespace CycloneDX +{ + /// + /// Allows to merge generic lists with items of specified types + /// (by default essentially adding entries which are not present + /// yet according to List.Contains() method), and calls special + /// logic for lists of BomEntry types. + /// Used in CycloneDX.Utils various Merge implementations as well + /// as in CycloneDX.Core BomEntity-derived classes' MergeWith(). + /// + /// Type of listed entries + public class ListMergeHelper + { + public List Merge(List list1, List list2) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + + if (typeof(BomEntity).IsInstanceOfType(list1[0])) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } + } + + // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + var result = new List(list1); + + foreach (var item in list2) + { + if (!(result.Contains(item))) + { + result.Add(item); + } + } + + return result; + } + } +} diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 71fc47b8..184c9239 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,60 +17,13 @@ using System; using System.Collections.Generic; +using CycloneDX; using CycloneDX.Models; using CycloneDX.Models.Vulnerabilities; using CycloneDX.Utils.Exceptions; namespace CycloneDX.Utils { - class ListMergeHelper - { - public List Merge(List list1, List list2) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; - - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; - - if (typeof(BomEntity).IsInstanceOfType(list1[0])) - { - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); - if (methodMerge != null) - { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); - } - else - { - // Should not get here, but if we do - log and fall through - if (iDebugLevel >= 1) - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - } - } - - // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - var result = new List(list1); - - foreach (var item in list2) - { - if (!(result.Contains(item))) - { - result.Add(item); - } - } - - return result; - } - } - public static partial class CycloneDXUtils { // TOTHINK: Now that we have a BomEntity base class, shouldn't From 1706389e462b4994a008a1c33d15ef2b201924b5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:38:42 +0200 Subject: [PATCH 045/285] CycloneDX.Core Model classes: make them all derivates of BomEntity so common equality, equivalence and merging methods are inherited and applied by default Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/AttachedText.cs | 2 +- src/CycloneDX.Core/Models/Bom.cs | 7 ++++++- src/CycloneDX.Core/Models/Commit.cs | 2 +- src/CycloneDX.Core/Models/Composition.cs | 2 +- src/CycloneDX.Core/Models/DataClassification.cs | 2 +- src/CycloneDX.Core/Models/Dependency.cs | 4 +++- src/CycloneDX.Core/Models/Diff.cs | 3 ++- src/CycloneDX.Core/Models/Evidence.cs | 2 +- src/CycloneDX.Core/Models/EvidenceCopyright.cs | 2 +- src/CycloneDX.Core/Models/ExternalReference.cs | 2 +- src/CycloneDX.Core/Models/IdentifiableAction.cs | 2 +- src/CycloneDX.Core/Models/Issue.cs | 2 +- src/CycloneDX.Core/Models/License.cs | 2 +- src/CycloneDX.Core/Models/LicenseChoice.cs | 2 +- src/CycloneDX.Core/Models/Metadata.cs | 2 +- src/CycloneDX.Core/Models/Note.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalContact.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalEntity.cs | 2 +- src/CycloneDX.Core/Models/Patch.cs | 2 +- src/CycloneDX.Core/Models/Pedigree.cs | 2 +- src/CycloneDX.Core/Models/Property.cs | 2 +- src/CycloneDX.Core/Models/ReleaseNotes.cs | 2 +- src/CycloneDX.Core/Models/Service.cs | 5 +++-- src/CycloneDX.Core/Models/Source.cs | 2 +- src/CycloneDX.Core/Models/Swid.cs | 2 +- src/CycloneDX.Core/Models/Tool.cs | 9 +++++---- src/CycloneDX.Core/Models/ValidationResult.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs | 2 +- .../Models/Vulnerabilities/AffectedVersions.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs | 2 +- .../Models/Vulnerabilities/Vulnerability.cs | 4 +++- 35 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/CycloneDX.Core/Models/AttachedText.cs b/src/CycloneDX.Core/Models/AttachedText.cs index 65f71fd0..b3626029 100644 --- a/src/CycloneDX.Core/Models/AttachedText.cs +++ b/src/CycloneDX.Core/Models/AttachedText.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class AttachedText + public class AttachedText : BomEntity { [XmlAttribute("content-type")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 53ddd363..393c0973 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -29,7 +29,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlRoot("bom", IsNullable=false)] [ProtoContract] - public class Bom + public class Bom : BomEntity { [XmlIgnore] public string BomFormat => "CycloneDX"; @@ -133,5 +133,10 @@ public int NonNullableVersion [ProtoMember(10)] public List Vulnerabilities { get; set; } public bool ShouldSerializeVulnerabilities() { return Vulnerabilities?.Count > 0; } + + // TODO: MergeWith() might be reasonable but is currently handled + // by several strategy implementations in CycloneDX.Utils Merge.cs + // so maybe there should be sub-classes or strategy arguments or + // properties to select one of those implementations at run-time?.. } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/Commit.cs b/src/CycloneDX.Core/Models/Commit.cs index bd0ea2d1..60efa995 100644 --- a/src/CycloneDX.Core/Models/Commit.cs +++ b/src/CycloneDX.Core/Models/Commit.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Commit + public class Commit : BomEntity { [XmlElement("uid")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index be2a55a0..c6643738 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 : BomEntity, IXmlSerializable { [ProtoContract] public enum AggregateType diff --git a/src/CycloneDX.Core/Models/DataClassification.cs b/src/CycloneDX.Core/Models/DataClassification.cs index 2cc2815a..a3df70a2 100644 --- a/src/CycloneDX.Core/Models/DataClassification.cs +++ b/src/CycloneDX.Core/Models/DataClassification.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DataClassification + public class DataClassification : BomEntity { [XmlAttribute("flow")] [ProtoMember(1, IsRequired=true)] diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index f2bd35c0..34b7dfb8 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("dependency")] [ProtoContract] - public class Dependency: IEquatable + public class Dependency : BomEntity { [XmlAttribute("ref")] [ProtoMember(1)] @@ -36,6 +36,7 @@ public class Dependency: IEquatable [ProtoMember(2)] public List Dependencies { get; set; } +/* public bool Equals(Dependency obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -45,5 +46,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/Diff.cs b/src/CycloneDX.Core/Models/Diff.cs index 3eda6e66..b2b5ddf4 100644 --- a/src/CycloneDX.Core/Models/Diff.cs +++ b/src/CycloneDX.Core/Models/Diff.cs @@ -21,11 +21,12 @@ namespace CycloneDX.Models { [ProtoContract] - public class Diff + public class Diff : BomEntity { [XmlElement("text")] [ProtoMember(1)] public AttachedText Text { get; set; } + [XmlElement("url")] [ProtoMember(2)] public string Url { get; set; } diff --git a/src/CycloneDX.Core/Models/Evidence.cs b/src/CycloneDX.Core/Models/Evidence.cs index dac69adc..c4ae5f38 100644 --- a/src/CycloneDX.Core/Models/Evidence.cs +++ b/src/CycloneDX.Core/Models/Evidence.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence")] [ProtoContract] - public class Evidence + public class Evidence : BomEntity { [XmlElement("licenses")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EvidenceCopyright.cs b/src/CycloneDX.Core/Models/EvidenceCopyright.cs index 03161b88..d045c22b 100644 --- a/src/CycloneDX.Core/Models/EvidenceCopyright.cs +++ b/src/CycloneDX.Core/Models/EvidenceCopyright.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class EvidenceCopyright + public class EvidenceCopyright : BomEntity { [XmlText] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index 97b2b709..84d12e5d 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores")] [ProtoContract] - public class ExternalReference + public class ExternalReference : BomEntity { [ProtoContract] public enum ExternalReferenceType diff --git a/src/CycloneDX.Core/Models/IdentifiableAction.cs b/src/CycloneDX.Core/Models/IdentifiableAction.cs index 8cc58365..205e65b2 100644 --- a/src/CycloneDX.Core/Models/IdentifiableAction.cs +++ b/src/CycloneDX.Core/Models/IdentifiableAction.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class IdentifiableAction + public class IdentifiableAction : BomEntity { private DateTime? _timestamp; [XmlElement("timestamp")] diff --git a/src/CycloneDX.Core/Models/Issue.cs b/src/CycloneDX.Core/Models/Issue.cs index d8f3f584..591a6eb8 100644 --- a/src/CycloneDX.Core/Models/Issue.cs +++ b/src/CycloneDX.Core/Models/Issue.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Issue + public class Issue : BomEntity { [ProtoContract] public enum IssueClassification diff --git a/src/CycloneDX.Core/Models/License.cs b/src/CycloneDX.Core/Models/License.cs index ee80a378..44438d62 100644 --- a/src/CycloneDX.Core/Models/License.cs +++ b/src/CycloneDX.Core/Models/License.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [XmlType("license")] [ProtoContract] - public class License + public class License : BomEntity { [XmlElement("id")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/LicenseChoice.cs b/src/CycloneDX.Core/Models/LicenseChoice.cs index 8d27a408..fec91a2e 100644 --- a/src/CycloneDX.Core/Models/LicenseChoice.cs +++ b/src/CycloneDX.Core/Models/LicenseChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class LicenseChoice + public class LicenseChoice : BomEntity { [XmlElement("license")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Metadata.cs b/src/CycloneDX.Core/Models/Metadata.cs index df9d7681..e6483af0 100644 --- a/src/CycloneDX.Core/Models/Metadata.cs +++ b/src/CycloneDX.Core/Models/Metadata.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Metadata + public class Metadata : BomEntity { private DateTime? _timestamp; [XmlElement("timestamp")] diff --git a/src/CycloneDX.Core/Models/Note.cs b/src/CycloneDX.Core/Models/Note.cs index feaea13e..66fd3292 100644 --- a/src/CycloneDX.Core/Models/Note.cs +++ b/src/CycloneDX.Core/Models/Note.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Note + public class Note : BomEntity { [XmlElement("locale")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalContact.cs b/src/CycloneDX.Core/Models/OrganizationalContact.cs index ab6943b1..148100e1 100644 --- a/src/CycloneDX.Core/Models/OrganizationalContact.cs +++ b/src/CycloneDX.Core/Models/OrganizationalContact.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalContact + public class OrganizationalContact : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalEntity.cs b/src/CycloneDX.Core/Models/OrganizationalEntity.cs index 4b80dd97..420333e4 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntity.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntity.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntity + public class OrganizationalEntity : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Patch.cs b/src/CycloneDX.Core/Models/Patch.cs index be9aa89f..704041a7 100644 --- a/src/CycloneDX.Core/Models/Patch.cs +++ b/src/CycloneDX.Core/Models/Patch.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Patch + public class Patch : BomEntity { [ProtoContract] public enum PatchClassification diff --git a/src/CycloneDX.Core/Models/Pedigree.cs b/src/CycloneDX.Core/Models/Pedigree.cs index 537d6e87..17609381 100644 --- a/src/CycloneDX.Core/Models/Pedigree.cs +++ b/src/CycloneDX.Core/Models/Pedigree.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Pedigree + public class Pedigree : BomEntity { [XmlArray("ancestors")] [XmlArrayItem("component")] diff --git a/src/CycloneDX.Core/Models/Property.cs b/src/CycloneDX.Core/Models/Property.cs index a61b45f2..5217e5d2 100644 --- a/src/CycloneDX.Core/Models/Property.cs +++ b/src/CycloneDX.Core/Models/Property.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Property + public class Property : BomEntity { [XmlAttribute("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ReleaseNotes.cs b/src/CycloneDX.Core/Models/ReleaseNotes.cs index b8a33ce9..d20f8d88 100644 --- a/src/CycloneDX.Core/Models/ReleaseNotes.cs +++ b/src/CycloneDX.Core/Models/ReleaseNotes.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ReleaseNotes + public class ReleaseNotes : BomEntity { [XmlElement("type")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 101e6fea..38afffcf 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Service: IEquatable + public class Service : BomEntity { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -123,7 +123,7 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } - +/* public bool Equals(Service obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -133,5 +133,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } diff --git a/src/CycloneDX.Core/Models/Source.cs b/src/CycloneDX.Core/Models/Source.cs index 1c6bbead..70c781fd 100644 --- a/src/CycloneDX.Core/Models/Source.cs +++ b/src/CycloneDX.Core/Models/Source.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Source + public class Source : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Swid.cs b/src/CycloneDX.Core/Models/Swid.cs index 4cd9eff8..84db40a2 100644 --- a/src/CycloneDX.Core/Models/Swid.cs +++ b/src/CycloneDX.Core/Models/Swid.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Swid + public class Swid : BomEntity { [XmlAttribute("tagId")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 60353ef7..8a3d5cde 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Tool: BomEntity + public class Tool : BomEntity { [XmlElement("vendor")] [ProtoMember(1)] @@ -46,17 +46,18 @@ public class Tool: BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } - +/* public bool Equals(Tool obj) { - /*return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj);*/ + //return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); return base.Equals(obj); } public override int GetHashCode() { - /*return CycloneDX.Json.Serializer.Serialize(this).GetHashCode();*/ + //return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); return base.GetHashCode(); } +*/ } } diff --git a/src/CycloneDX.Core/Models/ValidationResult.cs b/src/CycloneDX.Core/Models/ValidationResult.cs index 2580f86c..b1bb313e 100644 --- a/src/CycloneDX.Core/Models/ValidationResult.cs +++ b/src/CycloneDX.Core/Models/ValidationResult.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models /// /// The return type for all validation methods. /// - public class ValidationResult + public class ValidationResult : BomEntity { /// /// true if the document has been successfully validated. diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs index 18080bed..5e005c41 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Advisory + public class Advisory : BomEntity { [XmlAttribute("title")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs b/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs index f0fc31bc..c80aeca8 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class AffectedVersions + public class AffectedVersions : BomEntity { [XmlElement("version")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs index 9cf37d53..2e72c268 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Affects + public class Affects : BomEntity { [XmlElement("ref")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs index 7422d12a..6f494bf9 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Analysis + public class Analysis : BomEntity { [XmlElement("state")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs index bd9565c3..c9c59b6e 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Credits + public class Credits : BomEntity { [XmlArray("organizations")] [XmlArrayItem("organization")] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs index acb2be22..60f90628 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Rating + public class Rating : BomEntity { [XmlElement("source")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs index f0548218..cc2af0d7 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Reference + public class Reference : BomEntity { [XmlAttribute("id")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index eb184534..c3daddeb 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Vulnerability: IEquatable + public class Vulnerability : BomEntity { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -125,6 +125,7 @@ public DateTime? Updated public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } +/* public bool Equals(Vulnerability obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -134,5 +135,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } From 1b976a8872ebf531b50cd992720a5cb2dc84e722 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 20:56:00 +0200 Subject: [PATCH 046/285] BomEntity: forward from default Equals() and Equivalent() implems to class-customized ones if present - fix discovery, optimize GetType() call count Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index eaa59c89..086b95d8 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -200,7 +200,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equals", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -220,7 +220,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equivalent", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -240,7 +240,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("MergeWith", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -263,7 +263,8 @@ internal string SerializeEntity() // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class // so comes up empty, or has to jump through hoops...) - if (KnownTypeSerializers.TryGetValue(this.GetType(), out var methodSerializeThis)) + Type thisType = this.GetType(); + if (KnownTypeSerializers.TryGetValue(thisType, out var methodSerializeThis)) { var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); return res1; @@ -285,12 +286,13 @@ internal string SerializeEntity() /// True if two objects are deemed equal public bool Equals(BomEntity other) { - if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquals)) + Type thisType = this.GetType(); + if (KnownTypeEquals.TryGetValue(thisType, out var methodEquals)) { return (bool)methodEquals.Invoke(this, new object[] {other}); } - if (other is null || this.GetType() != other.GetType()) return false; + if (other is null || thisType != other.GetType()) return false; return this.SerializeEntity() == other.SerializeEntity(); } @@ -311,12 +313,18 @@ public override int GetHashCode() /// the same real-life entity, False otherwise. public bool Equivalent(BomEntity other) { - if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquivalent)) + Type thisType = this.GetType(); + if (KnownTypeEquivalent.TryGetValue(thisType, out var methodEquivalent)) { + // Note we do not check for null/type of "other" at this point + // since the derived classes define the logic of equivalence + // (possibly to other entity subtypes as well). return (bool)methodEquivalent.Invoke(this, new object[] {other}); } - return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); + // Note that here a default Equivalent() may call into custom Equals(), + // so the similar null/type sanity shecks are still relevant. + return (!(other is null) && (thisType == other.GetType()) && this.Equals(other)); } /// From 90b2713ecc3390f0e3494970540cb789d894a201 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:27:34 +0200 Subject: [PATCH 047/285] BomEntity: use KnownTypeMergeWith[] from BomEntityListMergeHelper<> to call customized handlers where applicable Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 086b95d8..ef9da66c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -80,6 +80,10 @@ public List Merge(List list1, List list2) List result = new List(list1); Type TType = list1[0].GetType(); + if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + methodMergeWith = null; + } foreach (var item2 in list2) { @@ -102,15 +106,25 @@ public List Merge(List list1, List list2) // methods defined or inherited as is suitable for // the particular entity type, hence much less code // and error-checking than there was in the PoC: - if (item1.MergeWith(item2)) + bool resMerge; + if (methodMergeWith != null) { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + } + else + { + resMerge = item1.MergeWith(item2); } // MergeWith() may throw BomEntityConflictException which we // want to propagate to users - their input data is confusing. // Probably should not throw BomEntityIncompatibleException // unless the lists truly are of mixed types. + + if (resMerge) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } } if (isContained) @@ -153,7 +167,7 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - static List KnownEntityTypes = + public static List KnownEntityTypes = new Func>(() => { List derived_types = new List(); @@ -172,7 +186,7 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - static Dictionary KnownTypeSerializers = + public static Dictionary KnownTypeSerializers = new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); @@ -193,7 +207,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeEquals = + public static Dictionary KnownTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -213,7 +227,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeEquivalent = + public static Dictionary KnownTypeEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); @@ -233,7 +247,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeMergeWith = + public static Dictionary KnownTypeMergeWith = new Func>(() => { Dictionary dict = new Dictionary(); From 815285c12fb37567a88d76660b0348ceedd6d439 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:30:54 +0200 Subject: [PATCH 048/285] CycloneDX.Core/Models/Component.cs: basic adjustment to be a BomEntity (inherit equality methods, adapt MergeWith()) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0150e7fa..f0cd2c76 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -199,6 +199,7 @@ public bool NonNullableModified public ReleaseNotes ReleaseNotes { get; set; } public bool ShouldSerializeReleaseNotes() { return ReleaseNotes != null; } +/* public bool Equals(Component obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -208,6 +209,7 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ public bool Equivalent(Component obj) { @@ -219,13 +221,25 @@ public bool MergeWith(Component obj) if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (this.Equals(obj)) + try { - if (iDebugLevel >= 1) + // Basic checks for null, type compatibility, + // equality and non-equivalence; throws for + // the hard stuff to implement in the catch: + bool resBase = base.MergeWith(obj); + if (resBase && iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); - return true; + } + return resBase; + } + catch (BomEntityConflictException) + { + // No-op to fall through below with less indentation } + // Custom logic to squash together two equivalent entries - + // with same BomRef value but something differing elsewhere if ( (this.BomRef != null && BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) From 37849b369be97089954c5608487180e5ff7b0230 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:34:55 +0200 Subject: [PATCH 049/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging levels for different traces Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index f0cd2c76..74cc3b2f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -249,7 +249,7 @@ public bool MergeWith(Component obj) if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly - if (iDebugLevel >= 1) + if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; @@ -270,7 +270,7 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { - if (iDebugLevel >= 2) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { @@ -304,7 +304,7 @@ public bool MergeWith(Component obj) objItem = ComponentScope.Null; } - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" @@ -312,7 +312,7 @@ public bool MergeWith(Component obj) { // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } @@ -323,7 +323,7 @@ public bool MergeWith(Component obj) ) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -345,7 +345,7 @@ public bool MergeWith(Component obj) bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); @@ -359,7 +359,7 @@ public bool MergeWith(Component obj) var propValObj = property.GetValue(obj); if (propValTmp == null && propValObj == null) { - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -378,7 +378,7 @@ public bool MergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount < 1) @@ -416,7 +416,7 @@ public bool MergeWith(Component obj) { if (methodEquals != null) { - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); propsSeemEqualLearned = true; @@ -425,7 +425,7 @@ public bool MergeWith(Component obj) catch (System.Exception exc) { // no-op - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } @@ -439,21 +439,21 @@ public bool MergeWith(Component obj) { try { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - if (iDebugLevel >= 6) + if (iDebugLevel >= 7) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added @@ -479,12 +479,12 @@ public bool MergeWith(Component obj) // e.g. 'Scope' helper // followed by 'Scope' // which we specially handle above - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); continue; } - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); @@ -508,7 +508,7 @@ public bool MergeWith(Component obj) { if (methodEquals != null) { - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; @@ -517,7 +517,7 @@ public bool MergeWith(Component obj) catch (System.Exception exc) { // no-op - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } @@ -526,7 +526,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; @@ -542,7 +542,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; @@ -555,7 +555,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqual) { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } @@ -572,7 +572,7 @@ public bool MergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } @@ -582,7 +582,7 @@ public bool MergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - if (iDebugLevel >= 6) + if (iDebugLevel >= 7) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } From 277d0daa4601e00722702c0a9528afdd6bbeb8a1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:59:22 +0200 Subject: [PATCH 050/285] CycloneDX.Core/Models/Component.cs: MergeWith(): speed-up with BomEntity.KnownEntityTypeProperties[] Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 16 ++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ef9da66c..fe517e8d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -181,6 +181,22 @@ public class BomEntity : IEquatable return derived_types; }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equals() method implementations + /// (if present), prepared startically at start time. + /// + public static Dictionary KnownEntityTypeProperties = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + dict[type] = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 74cc3b2f..2c8dceaa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -248,7 +248,7 @@ public bool MergeWith(Component obj) // merge the attribute values with help of reflection: if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); - PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; //this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); From f2e83b7f4f67e29b6957d1a75f3a2bfb6b977138 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:01:45 +0200 Subject: [PATCH 051/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 2c8dceaa..7bad243f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -227,9 +227,16 @@ public bool MergeWith(Component obj) // equality and non-equivalence; throws for // the hard stuff to implement in the catch: bool resBase = base.MergeWith(obj); - if (resBase && iDebugLevel >= 1) + if (iDebugLevel >= 1) { - Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); + if (resBase) + { + Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); + } + else + { + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + } } return resBase; } From 6c3b6671fc32b3a96eff0966436d6812b23a0c63 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:30:55 +0200 Subject: [PATCH 052/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 7bad243f..44ccd21c 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -235,7 +235,8 @@ public bool MergeWith(Component obj) } else { - Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); } } return resBase; From eb51a960182bba4b5874e2062f93ce67766268d6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:31:55 +0200 Subject: [PATCH 053/285] CycloneDX.Core/Models/Component.cs: rearrange processing of un-set (null) Scope value Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 44ccd21c..c3d93fdc 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -118,6 +118,8 @@ public ComponentScope NonNullableScope { get { + if (Scope == null) + return ComponentScope.Null; return Scope.Value; } set @@ -296,9 +298,14 @@ public bool MergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { // Unspecified => required per CycloneDX spec v1.4?.. + // Currently handled below like that, so (enum) Null value here. + tmpItem = ComponentScope.Null; + } + catch (System.Reflection.TargetInvocationException) + { tmpItem = ComponentScope.Null; } @@ -307,7 +314,11 @@ public bool MergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.Exception) + catch (System.InvalidOperationException) + { + objItem = ComponentScope.Null; + } + catch (System.Reflection.TargetInvocationException) { objItem = ComponentScope.Null; } From a43924625bd360d00229b3be7c4838faf87dad28 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:40:24 +0200 Subject: [PATCH 054/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c3d93fdc..53de54b8 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -625,7 +625,7 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related upon second look"); } // Merge was not applicable or otherwise did not succeed From cefd853f528d0e5d1bcbf7dc78bb26a873dc477a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 23:01:56 +0200 Subject: [PATCH 055/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise handling of various Scope values, refresh comments about spec versions involved Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 53de54b8..13ba2d01 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -251,7 +251,7 @@ public bool MergeWith(Component obj) // Custom logic to squash together two equivalent entries - // with same BomRef value but something differing elsewhere if ( - (this.BomRef != null && BomRef.Equals(obj.BomRef)) || + (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) ) { // Objects seem equivalent according to critical arguments; @@ -326,16 +326,38 @@ public bool MergeWith(Component obj) if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); - // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + // Since CycloneDX spec v1.0 up to at least v1.4, + // an absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { - // keep absent==required; upgrade optional objItem + // BOTH are not specified + if (tmpItem == ComponentScope.Null && objItem == ComponentScope.Null) + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SCOPE: keep unspecified explicitly"); + continue; + } + + if (tmpItem == ComponentScope.Optional && objItem == ComponentScope.Optional) + { + property.SetValue(tmp, ComponentScope.Optional); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SCOPE: keep 'Optional'"); + continue; + } + + // Any one (or both) are Required, or Null meaning required: + // keep absent=>required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } + // NOTE: "excluded" is only defined since CycloneDX spec v1.1 => + // you should not see it read from v1.0 documents. + // TOTHINK: Theoretically: what if we are asked to output a v1.0 + // document after merge of newer documents? Emitter should care... if ( (tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional) From 1624c20164d8c066ce214e20afa05bcbfc1d6a42 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 23:26:09 +0200 Subject: [PATCH 056/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise discovery of methodEquals and methodMergeWith via pre-cached BomEntity static Dicts Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 21 ++++++++++++ src/CycloneDX.Core/Models/Component.cs | 45 +++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index fe517e8d..0bf4bc5a 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -238,6 +238,27 @@ public class BomEntity : IEquatable return dict; }) (); + // Our loops check for some non-BomEntity typed value equalities, + // so cache their methods if present. Note that this one retains + // the "null" results to mark that we do not need to look further. + public static Dictionary KnownOtherTypeEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var listMore = new List(); + listMore.Add(typeof(string)); + listMore.Add(typeof(bool)); + listMore.Add(typeof(int)); + foreach (var type in listMore) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + dict[type] = method; + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about their custom Equivalent() method implementations diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 13ba2d01..9996c703 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -434,8 +434,26 @@ public bool MergeWith(Component obj) } var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + // No need to re-query now that we have BomEntity descendance: + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + methodMergeWith = null; + } + + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) + { + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + { + methodEquals = methodEquals2; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } + } for (int o = 0; o < propValObjCount; o++) { @@ -541,7 +559,20 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) + { + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + { + methodEquals = methodEquals2; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } + } + bool propsSeemEqual = false; bool propsSeemEqualLearned = false; @@ -600,7 +631,13 @@ public bool MergeWith(Component obj) Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + // No need to re-query now that we have BomEntity descendance: + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + methodMergeWith = null; + } + if (methodMergeWith != null) { try From 476e0e8e74f89d007eeb07c06dca6539829b5dc5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 00:27:24 +0200 Subject: [PATCH 057/285] Component.MergeWith() and ListMergeHelper.Merge(): use cached BomEntityListMergeHelperReflection and BomEntityListReflection Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 27 ++++++--- src/CycloneDX.Core/Models/BomEntity.cs | 83 ++++++++++++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 22 ++++++- 3 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index e9ab7adb..09ba178c 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -43,13 +44,25 @@ public List Merge(List list1, List list2) if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + MethodInfo methodMerge = null; + Object helper; + // Use cached info where available + if (BomEntity.KnownBomEntityListMergeHelpers.TryGetValue(typeof(T), out BomEntityListMergeHelperReflection refInfo)) + { + methodMerge = refInfo.methodMerge; + helper = refInfo.helperInstance; + } + else + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); + helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + } + if (methodMerge != null) { return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0bf4bc5a..cbf39e3f 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -146,6 +146,22 @@ public List Merge(List list1, List list2) } } + public class BomEntityListReflection + { + public Type genericType; + public PropertyInfo propCount; + public MethodInfo methodAdd; + public MethodInfo methodAddRange; + public MethodInfo methodGetItem; + } + + public class BomEntityListMergeHelperReflection + { + public Type genericType; + public MethodInfo methodMerge; + public Object helperInstance; + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -197,6 +213,73 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownEntityTypeLists = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listType = typeof(List<>); + Type constructedListType = listType.MakeGenericType(type); + // Needed? var helper = Activator.CreateInstance(constructedListType); + + dict[type] = new BomEntityListReflection(); + dict[type].genericType = constructedListType; + + // Gotta use reflection for run-time evaluated type methods: + dict[type].propCount = constructedListType.GetProperty("Count"); + dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); + dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); + dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + } + return dict; + }) (); + + public static Dictionary KnownBomEntityListMergeHelpers = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + Type constructedListHelperType = listHelperType.MakeGenericType(type); + var helper = Activator.CreateInstance(constructedListHelperType); + Type LType = null; + if (KnownEntityTypeLists.TryGetValue(type, out BomEntityListReflection refInfo)) + { + LType = refInfo.genericType; + } + + if (LType != null) + { + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType }); + if (methodMerge != null) + { + dict[type] = new BomEntityListMergeHelperReflection(); + dict[type].genericType = constructedListHelperType; + dict[type].methodMerge = methodMerge; + dict[type].helperInstance = helper; + // Callers would return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - make noise + throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a Merge() helper method"); + } + } + else + { + throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a List class definition"); + } + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 9996c703..f0005d66 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -406,9 +406,25 @@ public bool MergeWith(Component obj) } var LType = (propValTmp == null ? propValObj.GetType() : propValTmp.GetType()); - var propCount = LType.GetProperty("Count"); - var methodGetItem = LType.GetMethod("get_Item"); - var methodAdd = LType.GetMethod("Add"); + // Use cached info where available + PropertyInfo propCount = null; + MethodInfo methodGetItem = null; + MethodInfo methodAdd = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(LType, out BomEntityListReflection refInfo)) + { + propCount = refInfo.propCount; + methodGetItem = refInfo.methodGetItem; + methodAdd = refInfo.methodAdd; + } + else + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); + propCount = LType.GetProperty("Count"); + methodGetItem = LType.GetMethod("get_Item"); + methodAdd = LType.GetMethod("Add"); + } + if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) From 6c7795fecdea1d6168f6fb326d97efb939eafc95 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:14:32 +0200 Subject: [PATCH 058/285] Component.MergeWith(): handle Enum values quickly Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index f0005d66..45bc1c5c 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -575,6 +575,18 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); + + if (TType.IsEnum) + { + if (propValTmp == propValObj) + { + continue; + } + + mergedOk = false; + break; + } + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) From d564eb16f88b4f865e429206c9ea83ca4665e007 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:17:56 +0200 Subject: [PATCH 059/285] Component.MergeWith(): handle Enum values even more quickly Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 45bc1c5c..1941eaa6 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -544,6 +544,21 @@ public bool MergeWith(Component obj) } break; + // Default handling for enums, if not customized above + case Type _ when (property.PropertyType.IsEnum): + { + // Not nullable! + var propValTmp = property.GetValue(tmp, null); + var propValObj = property.GetValue(obj, null); + if (propValTmp == propValObj) + { + continue; + } + + mergedOk = false; + } + break; + default: { if ( @@ -575,18 +590,6 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); - - if (TType.IsEnum) - { - if (propValTmp == propValObj) - { - continue; - } - - mergedOk = false; - break; - } - if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) From dd4f04233e97a2d1bfa9ecf6895c760b7f6be234 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:34:55 +0200 Subject: [PATCH 060/285] Component.MergeWith(): try to avoid spurious values for null (missing in original JSON) NonNullable properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 1941eaa6..e56880d0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -271,6 +271,11 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { try { + // Avoid spurious "modified=false" in merged JSON + if (property.Name == "Modified" && !(this.Modified.HasValue)) + continue; + if (property.Name == "Scope" && !(this.Scope.HasValue)) + continue; property.SetValue(tmp, property.GetValue(this, null)); } catch (System.Exception) { // no-op From 49cfff7c30c5129e0d6ea86bbe90ab2083c2a5d1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:12:35 +0200 Subject: [PATCH 061/285] Component.MergeWith(): keep track of BomEntity-derived classes which use default Equals() and Equivalent() implementations Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 36 ++++++++++++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 33 ++++++++++++++++------- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cbf39e3f..8a6cae14 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -321,6 +321,24 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownDefaultEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var methodDefault = typeof(BomEntity).GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { typeof(BomEntity) }); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + if (method == null) + dict[type] = methodDefault; + } + return dict; + }) (); + // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. @@ -362,6 +380,24 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownDefaultEquivalent = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var methodDefault = typeof(BomEntity).GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { typeof(BomEntity) }); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + if (method == null) + dict[type] = methodDefault; + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about their custom MergeWith() method implementations diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index e56880d0..2fddced0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -423,7 +423,7 @@ public bool MergeWith(Component obj) } else { - if (iDebugLevel >= 4) + if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); propCount = LType.GetProperty("Count"); methodGetItem = LType.GetMethod("get_Item"); @@ -464,15 +464,22 @@ public bool MergeWith(Component obj) if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { - if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + if (KnownDefaultEquals.TryGetValue(TType, out var methodEquals2)) { methodEquals = methodEquals2; } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); - if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals3)) + { + methodEquals = methodEquals3; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } @@ -597,18 +604,26 @@ public bool MergeWith(Component obj) var TType = propValTmp.GetType(); if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { - if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + if (KnownDefaultEquals.TryGetValue(TType, out var methodEquals2)) { methodEquals = methodEquals2; } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); - if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals3)) + { + methodEquals = methodEquals3; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } + bool propsSeemEqual = false; bool propsSeemEqualLearned = false; From c945c459fb72abee336c21a36a58ff6e0a4a7a92 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:21:21 +0200 Subject: [PATCH 062/285] BomEntity: when tracking BomEntityListReflection[type] also leave a key for BomEntityListReflection[List] Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 8a6cae14..e48f1778 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -233,6 +233,10 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + + // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] + // TODO: Separate dict?.. + dict[constructedListType] = dict[type]; } return dict; }) (); From a55b30d39a47527d7dce6a2f0254145b602969b2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:36:40 +0200 Subject: [PATCH 063/285] Component.MergeWith(): skip changes due to un-set "other" Modified property Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 2fddced0..e8dcdd0e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -387,7 +387,9 @@ public bool MergeWith(Component obj) case Type _ when (property.Name == "NonNullableModified"): { - // Not nullable! + // Not nullable! Keep un-set if applicable. + if (!obj.Modified.HasValue) + continue; bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); From 6ac10ba614079f2dfc4bdc6df7785a35ea622c5a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:39:40 +0200 Subject: [PATCH 064/285] Component.MergeWith(): skip changes due to un-set "this+other" Scope property Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index e8dcdd0e..ccd5bda1 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -297,7 +297,10 @@ public bool MergeWith(Component obj) */ case Type _ when property.PropertyType == typeof(ComponentScope): { - // Not nullable! + // Not nullable! Quickly keep un-set if applicable. + if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) + continue; + ComponentScope tmpItem; try { From 510f4d1d7c428083fa6e5f3b51c87be078e73bb3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 11:56:17 +0200 Subject: [PATCH 065/285] Component.MergeWith(): update comments about Scope and NonNullableScope Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index ccd5bda1..0b352a9e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -297,6 +297,7 @@ public bool MergeWith(Component obj) */ case Type _ when property.PropertyType == typeof(ComponentScope): { + // NOTE: Intentionally not matching 'Scope' helper // Not nullable! Quickly keep un-set if applicable. if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) continue; @@ -584,7 +585,7 @@ public bool MergeWith(Component obj) ) { // e.g. 'Scope' helper - // followed by 'Scope' + // followed by '{ComponentScope NonNullableScope}' // which we specially handle above if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); From fb06139fa4cd40a460b993267d96b1e26ee93aa6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 11:57:36 +0200 Subject: [PATCH 066/285] Component.MergeWith(): interrupt list processing as soon as we have a hit (handled a "tmp" target entry which was equivalent or equal to an incoming "obj" entry) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0b352a9e..c2d49e80 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -496,7 +496,7 @@ public bool MergeWith(Component obj) continue; bool listHit = false; - for (int t = 0; t < propValTmpCount; t++) + for (int t = 0; t < propValTmpCount && !listHit; t++) { var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); if (tmpItem != null) From 006811e88a791cbded733eb0f4fe5c08cce600ba Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:13:01 +0200 Subject: [PATCH 067/285] Component.MergeWith(): when preparing the "tmp" clone, skip "NonNullable" helper properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c2d49e80..cb825b71 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -272,10 +272,15 @@ public bool MergeWith(Component obj) { try { // Avoid spurious "modified=false" in merged JSON - if (property.Name == "Modified" && !(this.Modified.HasValue)) + // Also skip helpers, care about real values + if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(this.Modified.HasValue)) { + // Can not set R/O prop: ### tmp.Modified.HasValue = false; continue; - if (property.Name == "Scope" && !(this.Scope.HasValue)) + } + if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(this.Scope.HasValue)) { + // Can not set R/O prop: ### tmp.Scope.HasValue = false; continue; + } property.SetValue(tmp, property.GetValue(this, null)); } catch (System.Exception) { // no-op From ac1523330189af3beab37de05076fe68a2e40870 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:13:55 +0200 Subject: [PATCH 068/285] Component.MergeWith(): when copying back from the "tmp" clone to "this", skip "NonNullable" helper properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index cb825b71..d5cb6f41 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -735,6 +735,16 @@ public bool MergeWith(Component obj) // No failures, only now update the current object: foreach (PropertyInfo property in properties) { + // Avoid spurious "modified=false" in merged JSON + // Also skip helpers, care about real values + if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(tmp.Modified.HasValue)) { + // Can not set R/O prop: ### this.Modified.HasValue = false; + continue; + } + if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(tmp.Scope.HasValue)) { + // Can not set R/O prop: ### this.Scope.HasValue = false; + continue; + } property.SetValue(this, property.GetValue(tmp, null)); } } From 9666e7afb7bfcdeb5b96b8c264590a8fe675d286 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:41:18 +0200 Subject: [PATCH 069/285] Component.MergeWith(): leave a TODO comment for code sharing eventually Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index d5cb6f41..fba5721b 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -250,6 +250,11 @@ public bool MergeWith(Component obj) // Custom logic to squash together two equivalent entries - // with same BomRef value but something differing elsewhere + // TODO: Much of this seems reusable - if other classes get + // a need for some fully-fledged MergeWith, consider breaking + // this code into helper methods and patterns, so that only + // specific property hits would be customized and the default + // scaffolding shared. if ( (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) From 9c7265802995c52a81e05cfd46b134de97b77f26 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 18:48:47 +0200 Subject: [PATCH 070/285] CycloneDX.Core/Json/Serializer.Serialization.cs: implement the one Serialize(BomEntity) to rule them all Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index 2348fc9d..d99f4500 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -61,7 +61,43 @@ public static string Serialize(Bom bom) internal static string Serialize(BomEntity entity) { Contract.Requires(entity != null); - return JsonSerializer.Serialize(entity, _options); + // Default code tends to return serialization of base class + // => empty (no props in BomEntity itself) so we have to + // coerce it into seeing the object type we need to parse. + string res = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(entity.GetType(), out var listInfo) + && listInfo != null && listInfo.genericType != null + && listInfo.methodAdd != null && listInfo.methodGetItem != null + ) { + var castList = Activator.CreateInstance(listInfo.genericType); + listInfo.methodAdd.Invoke(castList, new object[] { entity }); + res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), _options); + } + else + { + var castEntity = Convert.ChangeType(entity, entity.GetType()); +/* + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } +*/ + res = JsonSerializer.Serialize(castEntity, _options); + } + return res; } internal static string Serialize(Component component) From 08270206bcb515a96bcf769aff1d3c783757b0f8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 18:49:24 +0200 Subject: [PATCH 071/285] CycloneDX.Core/Json/Serializer.Serialization.cs: comment away other internal Serialize() implementations Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Serializer.Serialization.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index d99f4500..a3742df6 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -99,7 +99,7 @@ internal static string Serialize(BomEntity entity) } return res; } - +/* internal static string Serialize(Component component) { Contract.Requires(component != null); @@ -129,5 +129,6 @@ internal static string Serialize(Models.Vulnerabilities.Vulnerability vulnerabil Contract.Requires(vulnerability != null); return JsonSerializer.Serialize(vulnerability, _options); } +*/ } } From 8f1be8c8fd76082f873be794180aa9e862a6b74e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:33:16 +0200 Subject: [PATCH 072/285] CycloneDX.Core/Json/Serializer.Serialization.cs: tidy up Serialize(BomEntity implem) Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index a3742df6..21c8ddbb 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -58,12 +58,20 @@ public static string Serialize(Bom bom) return jsonBom; } + /// + /// Return serialization of a class derived from BomEntity. + /// + /// A BomEntity-derived class + /// String with JSON markup internal static string Serialize(BomEntity entity) { Contract.Requires(entity != null); // Default code tends to return serialization of base class // => empty (no props in BomEntity itself) so we have to // coerce it into seeing the object type we need to parse. + // This codepath is critical for us since serialization is + // used to compare if entities are Equal() in massive loops + // when merging Bom's. Optimizations welcome. string res = null; if (BomEntity.KnownEntityTypeLists.TryGetValue(entity.GetType(), out var listInfo) && listInfo != null && listInfo.genericType != null @@ -76,29 +84,11 @@ internal static string Serialize(BomEntity entity) else { var castEntity = Convert.ChangeType(entity, entity.GetType()); -/* - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); - if (methodMerge != null) - { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); - } - else - { - // Should not get here, but if we do - log and fall through - if (iDebugLevel >= 1) - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - } -*/ res = JsonSerializer.Serialize(castEntity, _options); } return res; } + /* internal static string Serialize(Component component) { From 5adb785aebbe485f0e78b97ac074106666afeb1e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:47:17 +0200 Subject: [PATCH 073/285] CycloneDX.Core/Json/Serializer.Serialization.cs: introduce SerializeCompact(BomEntity) with minimal markup overhead and use it in BomEntity.Equals() Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 39 +++++++++++++++++-- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index 21c8ddbb..d7cc2fc1 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -33,6 +33,14 @@ namespace CycloneDX.Json public static partial class Serializer { private static JsonSerializerOptions _options = Utils.GetJsonSerializerOptions(); + private static readonly JsonSerializerOptions _options_compact = new Func(() => + { + JsonSerializerOptions opts = Utils.GetJsonSerializerOptions(); + opts.AllowTrailingCommas = false; + opts.WriteIndented = false; + + return opts; + }) (); /// /// Serializes a CycloneDX BOM writing the output to a stream. @@ -59,11 +67,36 @@ public static string Serialize(Bom bom) } /// - /// Return serialization of a class derived from BomEntity. + /// Return serialization of a class derived from BomEntity + /// with common JsonSerializerOptions defined for this class. /// /// A BomEntity-derived class /// String with JSON markup internal static string Serialize(BomEntity entity) + { + return Serialize(entity, _options); + } + + /// + /// Return serialization of a class derived from BomEntity + /// with compact JsonSerializerOptions aimed at minimal + /// markup (harder to read for humans, less bytes to parse). + /// + /// A BomEntity-derived class + /// String with JSON markup + internal static string SerializeCompact(BomEntity entity) + { + return Serialize(entity, _options_compact); + } + + /// + /// Return serialization of a class derived from BomEntity + /// with caller-specified JsonSerializerOptions. + /// + /// A BomEntity-derived class + /// Options for serializer + /// String with JSON markup + internal static string Serialize(BomEntity entity, JsonSerializerOptions jserOptions) { Contract.Requires(entity != null); // Default code tends to return serialization of base class @@ -79,12 +112,12 @@ internal static string Serialize(BomEntity entity) ) { var castList = Activator.CreateInstance(listInfo.genericType); listInfo.methodAdd.Invoke(castList, new object[] { entity }); - res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), _options); + res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), jserOptions); } else { var castEntity = Convert.ChangeType(entity, entity.GetType()); - res = JsonSerializer.Serialize(castEntity, _options); + res = JsonSerializer.Serialize(castEntity, jserOptions); } return res; } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e48f1778..6bde6c97 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -444,7 +444,7 @@ internal string SerializeEntity() return res1; } - var res = CycloneDX.Json.Serializer.Serialize(this); + var res = CycloneDX.Json.Serializer.SerializeCompact(this); return res; } From ebd6fddcd258b950e8809a74d11e043dc6cf7294 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:53:34 +0200 Subject: [PATCH 074/285] BomEntity: avoid caching in KnownTypeSerializers any default implementations for BomEntity itself as the handler for derived classes Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 6bde6c97..26dca1e6 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -293,13 +293,16 @@ public class BomEntity : IEquatable new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); + var methodDefault = jserClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { typeof(BomEntity) }); Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { var method = jserClassType.GetMethod("Serialize", - BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, new Type[] { type }); - if (method != null) + if (method != null && method != methodDefault) dict[type] = method; } return dict; From e54ef7fa32c6db437690646c709508b098c19419 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:17:36 +0200 Subject: [PATCH 075/285] Component.MergeWith(): fix comparison of enums Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index fba5721b..28f10735 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -578,7 +578,7 @@ public bool MergeWith(Component obj) // Not nullable! var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); - if (propValTmp == propValObj) + if (propValTmp == propValObj || propValTmp.Equals(propValObj)) { continue; } From a5db31900308d96786e3bba8e46c4e14fa4471d4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:17:54 +0200 Subject: [PATCH 076/285] Component.MergeWith(): optimize detection of Nullable a bit Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 28f10735..4b8cbf90 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -591,6 +591,7 @@ public bool MergeWith(Component obj) { if ( /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ + property.PropertyType.Name.StartsWith("Nullable") || property.PropertyType.ToString().StartsWith("System.Nullable") ) { From 423cf2b324cf7926a785d9b1a8a05fa47460002f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:29:43 +0200 Subject: [PATCH 077/285] Component.MergeWith(): fix comparison of stubborn enums Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 4b8cbf90..96e394c8 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -578,7 +578,8 @@ public bool MergeWith(Component obj) // Not nullable! var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); - if (propValTmp == propValObj || propValTmp.Equals(propValObj)) + // For some reason, reflected enums do not like getting compared + if (propValTmp == propValObj || propValTmp.Equals(propValObj) || ((Enum)propValTmp).CompareTo((Enum)propValObj) == 0 || propValTmp.ToString().Equals(propValObj.ToString())) { continue; } From 03f967a12692c26a25d1306472fdbe4a4b6a3b43 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 23:28:20 +0200 Subject: [PATCH 078/285] Introduce BomEntityListMergeHelperStrategy to tweak run-time behaviors Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 7 ++++- src/CycloneDX.Core/Models/BomEntity.cs | 38 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 09ba178c..d426139d 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -35,6 +35,11 @@ namespace CycloneDX public class ListMergeHelper { public List Merge(List list1, List list2) + { + return Merge(list1, list2, BomEntityListMergeHelperStrategy.Default()); + } + + public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -42,7 +47,7 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (typeof(BomEntity).IsInstanceOfType(list1[0])) + if (listMergeHelperStrategy.useBomEntityMerge && typeof(BomEntity).IsInstanceOfType(list1[0])) { MethodInfo methodMerge = null; Object helper; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 26dca1e6..789edb8c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -64,6 +64,44 @@ public BomEntityIncompatibleException(string msg, Type type1, Type type2) { } } + /// + /// Global configuration helper for ListMergeHelper, + /// BomEntityListMergeHelper, Merge.cs implementations + /// and related codebase. + /// + public class BomEntityListMergeHelperStrategy + { + /// + /// Cause ListMergeHelper to consider calling + /// the BomEntityListMergeHelper->Merge which in + /// turn calls BomEntity->MergeWith() in a loop, + /// vs. just comparing entities for equality and + /// deduplicating based on that (goes faster but + /// may cause data structure not conforming to spec) + /// + public bool useBomEntityMerge; + + + /// + /// CycloneDX spec version. + /// + public SpecificationVersion specificationVersion; + + /// + /// Return reasonable default strategy settings. + /// + /// A new ListMergeHelperStrategy instance + /// which the callers can tune to their liking. + public static BomEntityListMergeHelperStrategy Default() + { + return new BomEntityListMergeHelperStrategy() + { + useBomEntityMerge = true, + specificationVersion = SpecificationVersionHelpers.CurrentVersion + }; + } + } + public class BomEntityListMergeHelper where T : BomEntity { public List Merge(List list1, List list2) From 29614e0a8b8f86e64da591410056a81cbf680412 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 23:44:14 +0200 Subject: [PATCH 079/285] Merge.cs: Use BomEntityListMergeHelperStrategy for quick FlatMerge() over the big population of Boms, and a clean-up pass in the end to deduplicate equivalent but unequal entries Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 44 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 184c9239..c35008d6 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -51,6 +51,11 @@ public static partial class CycloneDXUtils /// /// public static Bom FlatMerge(Bom bom1, Bom bom2) + { + return FlatMerge(bom1, bom2, BomEntityListMergeHelperStrategy.Default()); + } + + public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -67,14 +72,14 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) }; var toolsMerger = new ListMergeHelper(); - var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); + var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools, listMergeHelperStrategy); if (tools != null) { result.Metadata.Tools = tools; } var componentsMerger = new ListMergeHelper(); - result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); + result.Components = componentsMerger.Merge(bom1.Components, bom2.Components, listMergeHelperStrategy); // Add main component from bom2 as a "yet another component" // if missing in that list so far. Note: any more complicated @@ -110,19 +115,19 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) } var servicesMerger = new ListMergeHelper(); - result.Services = servicesMerger.Merge(bom1.Services, bom2.Services); + result.Services = servicesMerger.Merge(bom1.Services, bom2.Services, listMergeHelperStrategy); var extRefsMerger = new ListMergeHelper(); - result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences); + result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences, listMergeHelperStrategy); var dependenciesMerger = new ListMergeHelper(); - result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies); + result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies, listMergeHelperStrategy); var compositionsMerger = new ListMergeHelper(); - result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions); + result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions, listMergeHelperStrategy); var vulnerabilitiesMerger = new ListMergeHelper(); - result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities); + result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities, listMergeHelperStrategy); result = CleanupMetadataComponent(result); result = CleanupEmptyLists(result); @@ -165,17 +170,31 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); + BomEntityListMergeHelperStrategy quickStrategy = BomEntityListMergeHelperStrategy.Default(); + quickStrategy.useBomEntityMerge = false; // Note: we were asked to "merge" and so we do, per principle of // least surprise - even if there is just one entry in boms[] so // we might be inclined to skip the loop. Resulting document WILL // differ from such single original (serialNumber, timestamp...) + int countBoms = 0; foreach (var bom in boms) { - result = FlatMerge(result, bom); + result = FlatMerge(result, bom, quickStrategy); + countBoms++; } - if (bomSubject != null) + // The quickly-made merged Bom is likely messy (only deduplicating + // identical entries). Run another merge, careful this time, over + // the resulting collection with a lot fewer items to inspect with + // the heavier logic. + if (bomSubject is null) + { + var emptyBom = new Bom(); + result = FlatMerge(emptyBom, result, safeStrategy); + } + else { // use the params provided if possible: prepare a new document // with desired "metadata/component" and merge differing data @@ -184,12 +203,15 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) resultSubj.Metadata.Component = bomSubject; resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); - result = FlatMerge(resultSubj, result); + result = FlatMerge(resultSubj, result, safeStrategy); var mainDependency = new Dependency(); mainDependency.Ref = result.Metadata.Component.BomRef; mainDependency.Dependencies = new List(); - + + // Revisit original Boms which had a metadata/component + // to write them up as dependencies of newly injected + // top-level product name. foreach (var bom in boms) { if (!(bom.Metadata?.Component is null)) From 00a8ac52fca722f90560c662f7dfa5234a7f25ae Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 00:04:32 +0200 Subject: [PATCH 080/285] Merge.cs, BomEntity.cs: implement BomEntityListMergeHelperStrategy for quick and careless merges in BomEntityListMergeHelper class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 6 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 44 ++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index d426139d..ab0cd623 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -47,7 +47,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (listMergeHelperStrategy.useBomEntityMerge && typeof(BomEntity).IsInstanceOfType(list1[0])) + if (typeof(BomEntity).IsInstanceOfType(list1[0])) { MethodInfo methodMerge = null; Object helper; @@ -65,12 +65,12 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: - methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); } if (methodMerge != null) { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + return (List)methodMerge.Invoke(helper, new object[] {list1, list2, listMergeHelperStrategy}); } else { diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 789edb8c..a5374f4b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -104,7 +104,7 @@ public static BomEntityListMergeHelperStrategy Default() public class BomEntityListMergeHelper where T : BomEntity { - public List Merge(List list1, List list2) + public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { //return BomUtils.MergeBomEntityLists(list1, list2); if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) @@ -113,8 +113,46 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; + if (!listMergeHelperStrategy.useBomEntityMerge) + { + // Most BomEntity classes are not individually IEquatable to avoid the + // copy-paste coding overhead, however they inherit the Equals() and + // GetHashCode() methods from their base class. + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + + List hashList = new List(); + List resultQ = new List(); + + // Exclude possibly pre-existing identical entries first, then similarly + // handle data from the second list. Here we have the "benefit" of lack + // of real content merging, so already saved items (and their hashes) + // can be treated as immutable. + foreach (T item1 in list1) + { + if (item1 is null) continue; + int hash1 = item1.GetHashCode(); + if (hashList.Contains(hash1)) + continue; + resultQ.Add(item1); + hashList.Add(hash1); + } + + foreach (T item2 in list2) + { + if (item2 is null) continue; + int hash2 = item2.GetHashCode(); + if (hashList.Contains(hash2)) + continue; + resultQ.Add(item2); + hashList.Add(hash2); + } + + return resultQ; + } + if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for BomEntity derivatives: {list1.GetType().ToString()}"); + Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {list1.GetType().ToString()}"); List result = new List(list1); Type TType = list1[0].GetType(); @@ -299,7 +337,7 @@ public class BomEntity : IEquatable if (LType != null) { // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType }); + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); if (methodMerge != null) { dict[type] = new BomEntityListMergeHelperReflection(); From 9ff40f05610b5e8aea62c27ba29b38fb5b8a72c9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 01:21:38 +0200 Subject: [PATCH 081/285] Merge.cs, BomEntity.cs: refactor BomEntityListMergeHelperStrategy for slow careful merges in BomEntityListMergeHelper class, to also dedup input list1 and to not skip work due to null/empty inputs Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 18 ++- src/CycloneDX.Core/Models/BomEntity.cs | 201 ++++++++++++++++--------- 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index ab0cd623..486d2ded 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -44,10 +44,16 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + // Rule out utterly empty inputs + if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) + { + if (list1 is not null) return list1; + if (list2 is not null) return list2; + return new List(); + } - if (typeof(BomEntity).IsInstanceOfType(list1[0])) + // At least one of these entries exists, per above sanity check + if (typeof(BomEntity).IsInstanceOfType((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0])) { MethodInfo methodMerge = null; Object helper; @@ -82,7 +88,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"List-Merge for legacy types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + var result = new List(list1); foreach (var item in list2) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a5374f4b..ebe55899 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -81,7 +81,6 @@ public class BomEntityListMergeHelperStrategy /// public bool useBomEntityMerge; - /// /// CycloneDX spec version. /// @@ -106,116 +105,182 @@ public class BomEntityListMergeHelper where T : BomEntity { public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { - //return BomUtils.MergeBomEntityLists(list1, list2); if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + // Rule out utterly empty inputs + if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) + { + if (list1 is not null) return list1; + if (list2 is not null) return list2; + return new List(); + } + + List result = new List(); + // Note: no blind checks for null/empty inputs - part of logic below, + // in order to surely de-duplicate even single incoming lists. if (!listMergeHelperStrategy.useBomEntityMerge) { // Most BomEntity classes are not individually IEquatable to avoid the // copy-paste coding overhead, however they inherit the Equals() and // GetHashCode() methods from their base class. if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); List hashList = new List(); - List resultQ = new List(); + List hashList2 = new List(); // Exclude possibly pre-existing identical entries first, then similarly // handle data from the second list. Here we have the "benefit" of lack // of real content merging, so already saved items (and their hashes) // can be treated as immutable. - foreach (T item1 in list1) + if (!(list1 is null) && list1.Count > 0) { - if (item1 is null) continue; - int hash1 = item1.GetHashCode(); - if (hashList.Contains(hash1)) - continue; - resultQ.Add(item1); - hashList.Add(hash1); + foreach (T item1 in list1) + { + if (item1 is null) continue; + int hash1 = item1.GetHashCode(); + if (hashList.Contains(hash1)) + { + if (iDebugLevel >= 1) + Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list1: ${item1.SerializeEntity()}"); + continue; + } + result.Add(item1); + hashList.Add(hash1); + } } - foreach (T item2 in list2) + if (!(list2 is null) && list2.Count > 0) { - if (item2 is null) continue; - int hash2 = item2.GetHashCode(); - if (hashList.Contains(hash2)) - continue; - resultQ.Add(item2); - hashList.Add(hash2); + foreach (T item2 in list2) + { + if (item2 is null) continue; + int hash2 = item2.GetHashCode(); + + // For info (track if data is bad or hash is unreliably weak): + if (iDebugLevel >= 1) + { + if (hashList2.Contains(hash2)) + { + Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list2: ${item2.SerializeEntity()}"); + } + hashList2.Add(hash2); + } + + if (hashList.Contains(hash2)) + continue; + result.Add(item2); + hashList.Add(hash2); + } } - return resultQ; + return result; } + // Here both lists are assumed to possibly have same or equivalent + // entries, even inside the same original list (e.g. if prepared by + // quick logic above for de-duplicating the major bulk of content). + Type TType = ((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0]).GetType(); + if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {list1.GetType().ToString()}"); + Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {TType.ToString()}"); - List result = new List(list1); - Type TType = list1[0].GetType(); if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { methodMergeWith = null; } - foreach (var item2 in list2) + // Compact version of loop below; see comments there. + // In short, we avoid making a plain copy of list1 so + // we can carefully pass each entry to MergeWith() + // any suitable other in the same original list. + if (!(list1 is null) && list1.Count > 0) { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + foreach (var item0 in list1) + { + for (int i=0; i < result.Count; i++) + { + var item1 = result[i]; + bool resMerge; + if (methodMergeWith != null) + { + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0}); + } + else + { + resMerge = item1.MergeWith(item0); + } - for (int i=0; i < result.Count; i++) + if (resMerge) + { + break; // item2 merged into result[item1] or already equal to it + } + } + } + } + + // Similar logic to the pass above, but with optional logging to + // highlight results of merges of the second list into the first. + if (!(list2 is null) && list2.Count > 0) + { + foreach (var item2 in list2) { + bool isContained = false; if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - var item1 = result[i]; - - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there. - // For BomEntity descendant instances we assume that - // they have Equals(), Equivalent() and MergeWith() - // methods defined or inherited as is suitable for - // the particular entity type, hence much less code - // and error-checking than there was in the PoC: - bool resMerge; - if (methodMergeWith != null) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + + for (int i=0; i < result.Count; i++) { - resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + var item1 = result[i]; + + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + bool resMerge; + if (methodMergeWith != null) + { + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + } + else + { + resMerge = item1.MergeWith(item2); + } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. + + if (resMerge) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } } - else + + if (isContained) { - resMerge = item1.MergeWith(item2); + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } - // MergeWith() may throw BomEntityConflictException which we - // want to propagate to users - their input data is confusing. - // Probably should not throw BomEntityIncompatibleException - // unless the lists truly are of mixed types. - - if (resMerge) + else { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); } } - - if (isContained) - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); - } - else - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } } return result; From e297e974d3d48a8be13a0cdf15dc35fea06a5bb1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 01:25:24 +0200 Subject: [PATCH 082/285] Merge.cs, BomEntity.cs et al: clean up commented-away experimental and obsoleted code before PR Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 32 ------------------- src/CycloneDX.Core/Models/Component.cs | 25 +++------------ src/CycloneDX.Core/Models/Dependency.cs | 12 ------- src/CycloneDX.Core/Models/Service.cs | 11 ------- src/CycloneDX.Core/Models/Tool.cs | 13 -------- .../Models/Vulnerabilities/Vulnerability.cs | 12 ------- 6 files changed, 5 insertions(+), 100 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index d7cc2fc1..cd862512 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -121,37 +121,5 @@ internal static string Serialize(BomEntity entity, JsonSerializerOptions jserOpt } return res; } - -/* - internal static string Serialize(Component component) - { - Contract.Requires(component != null); - return JsonSerializer.Serialize(component, _options); - } - - internal static string Serialize(Dependency dependency) - { - Contract.Requires(dependency != null); - return JsonSerializer.Serialize(dependency, _options); - } - - internal static string Serialize(Service service) - { - Contract.Requires(service != null); - return JsonSerializer.Serialize(service, _options); - } - - internal static string Serialize(Tool tool) - { - Contract.Requires(tool != null); - return JsonSerializer.Serialize(tool, _options); - } - - internal static string Serialize(Models.Vulnerabilities.Vulnerability vulnerability) - { - Contract.Requires(vulnerability != null); - return JsonSerializer.Serialize(vulnerability, _options); - } -*/ } } diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 96e394c8..74373c8d 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -201,18 +201,6 @@ public bool NonNullableModified public ReleaseNotes ReleaseNotes { get; set; } public bool ShouldSerializeReleaseNotes() { return ReleaseNotes != null; } -/* - public bool Equals(Component obj) - { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - } - - public override int GetHashCode() - { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - } -*/ - public bool Equivalent(Component obj) { return (!(obj is null) && this.BomRef == obj.BomRef); @@ -263,14 +251,14 @@ public bool MergeWith(Component obj) // merge the attribute values with help of reflection: if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); - PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; //this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); - /* This fails due to copy of "non-null" fields which may be null: + /* This copier fails due to copy of "non-null" fields which may be null: * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ foreach (PropertyInfo property in properties) @@ -301,10 +289,7 @@ public bool MergeWith(Component obj) { case Type _ when property.PropertyType == typeof(Nullable): break; -/* - case Type _ when property.PropertyType == typeof(Nullable[]): - break; -*/ + case Type _ when property.PropertyType == typeof(ComponentScope): { // NOTE: Intentionally not matching 'Scope' helper @@ -591,7 +576,6 @@ public bool MergeWith(Component obj) default: { if ( - /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ property.PropertyType.Name.StartsWith("Nullable") || property.PropertyType.ToString().StartsWith("System.Nullable") ) @@ -641,7 +625,8 @@ public bool MergeWith(Component obj) } } - + // Track the result of comparison, and if we did find and + // run a method for comparison (the result was "learned"): bool propsSeemEqual = false; bool propsSeemEqualLearned = false; diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 34b7dfb8..70a2e948 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -35,17 +35,5 @@ public class Dependency : BomEntity [XmlElement("dependency")] [ProtoMember(2)] public List Dependencies { get; set; } - -/* - public bool Equals(Dependency 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/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 38afffcf..9de775b3 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -123,16 +123,5 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } -/* - public bool Equals(Service 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.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 8a3d5cde..6674462c 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -46,18 +46,5 @@ public class Tool : BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } -/* - public bool Equals(Tool obj) - { - //return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - return base.Equals(obj); - } - - public override int GetHashCode() - { - //return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - return base.GetHashCode(); - } -*/ } } diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index c3daddeb..372e8247 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -124,17 +124,5 @@ public DateTime? Updated [ProtoMember(18)] public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } - -/* - public bool Equals(Vulnerability obj) - { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - } - - public override int GetHashCode() - { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - } -*/ } } From 8f0bf430d58a5eb2bff81ecf6761e2558d0dc371 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 02:42:06 +0200 Subject: [PATCH 083/285] Merge.cs, BomEntity.cs et al: address CI complaints (code style, etc.) ...and the bike-shed must be green! Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 28 +++- src/CycloneDX.Core/Models/BomEntity.cs | 176 +++++++++++++++++-------- src/CycloneDX.Core/Models/Component.cs | 122 ++++++++++++++--- src/CycloneDX.Core/Models/Hash.cs | 16 +-- src/CycloneDX.Utils/Merge.cs | 93 ++++++++++--- 5 files changed, 334 insertions(+), 101 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 486d2ded..553f6067 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -42,13 +42,21 @@ public List Merge(List list1, List list2) public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) return list1; - if (list2 is not null) return list2; + if (list1 is not null) + { + return list1; + } + if (list2 is not null) + { + return list2; + } return new List(); } @@ -71,7 +79,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: - methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new [] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); } if (methodMerge != null) @@ -82,16 +90,26 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { // Should not get here, but if we do - log and fall through if (iDebugLevel >= 1) + { Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } } } // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge for legacy types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + } - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + if (list1 is null || list1.Count < 1) + { + return list2; + } + if (list2 is null || list2.Count < 1) + { + return list1; + } var result = new List(list1); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ebe55899..bdc5f58b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -28,11 +28,11 @@ namespace CycloneDX.Models public class BomEntityConflictException : Exception { public BomEntityConflictException() - : base(String.Format("Unresolvable conflict in Bom entities")) + : base("Unresolvable conflict in Bom entities") { } public BomEntityConflictException(Type type) - : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type.ToString())) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type)) { } public BomEntityConflictException(string msg) @@ -40,7 +40,7 @@ public BomEntityConflictException(string msg) { } public BomEntityConflictException(string msg, Type type) - : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type, msg)) { } } @@ -48,11 +48,11 @@ public BomEntityConflictException(string msg, Type type) public class BomEntityIncompatibleException : Exception { public BomEntityIncompatibleException() - : base(String.Format("Comparing incompatible Bom entities")) + : base("Comparing incompatible Bom entities") { } public BomEntityIncompatibleException(Type type1, Type type2) - : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1.ToString(), type2.ToString())) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1, type2)) { } public BomEntityIncompatibleException(string msg) @@ -60,7 +60,7 @@ public BomEntityIncompatibleException(string msg) { } public BomEntityIncompatibleException(string msg, Type type1, Type type2) - : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1.ToString(), type2.ToString(), msg)) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1, type2, msg)) { } } @@ -93,7 +93,7 @@ public class BomEntityListMergeHelperStrategy /// which the callers can tune to their liking. public static BomEntityListMergeHelperStrategy Default() { - return new BomEntityListMergeHelperStrategy() + return new BomEntityListMergeHelperStrategy { useBomEntityMerge = true, specificationVersion = SpecificationVersionHelpers.CurrentVersion @@ -106,13 +106,21 @@ public class BomEntityListMergeHelper where T : BomEntity public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) return list1; - if (list2 is not null) return list2; + if (list1 is not null) + { + return list1; + } + if (list2 is not null) + { + return list2; + } return new List(); } @@ -126,7 +134,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // copy-paste coding overhead, however they inherit the Equals() and // GetHashCode() methods from their base class. if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + } List hashList = new List(); List hashList2 = new List(); @@ -139,12 +149,17 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (T item1 in list1) { - if (item1 is null) continue; + if (item1 is null) + { + continue; + } int hash1 = item1.GetHashCode(); if (hashList.Contains(hash1)) { if (iDebugLevel >= 1) + { Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list1: ${item1.SerializeEntity()}"); + } continue; } result.Add(item1); @@ -156,7 +171,10 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (T item2 in list2) { - if (item2 is null) continue; + if (item2 is null) + { + continue; + } int hash2 = item2.GetHashCode(); // For info (track if data is bad or hash is unreliably weak): @@ -170,7 +188,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } if (hashList.Contains(hash2)) + { continue; + } result.Add(item2); hashList.Add(hash2); } @@ -185,7 +205,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat Type TType = ((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0]).GetType(); if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {TType.ToString()}"); + } if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { @@ -229,12 +251,16 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { bool isContained = false; if (iDebugLevel >= 3) + { Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + } for (int i=0; i < result.Count; i++) { if (iDebugLevel >= 3) + { Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + } var item1 = result[i]; // Squash contents of the new entry with an already @@ -270,14 +296,18 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (isContained) { if (iDebugLevel >= 2) + { Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } } else { // Add new entry "as is" (new-ness is subject to // equality checks of respective classes): if (iDebugLevel >= 2) + { Console.WriteLine($"WILL ADD: {item2.ToString()}"); + } result.Add(item2); } } @@ -324,7 +354,7 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - public static List KnownEntityTypes = + public static readonly List KnownEntityTypes = new Func>(() => { List derived_types = new List(); @@ -343,7 +373,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownEntityTypeProperties = + public static readonly Dictionary KnownEntityTypeProperties = new Func>(() => { Dictionary dict = new Dictionary(); @@ -354,7 +384,7 @@ public class BomEntity : IEquatable return dict; }) (); - public static Dictionary KnownEntityTypeLists = + public static readonly Dictionary KnownEntityTypeLists = new Func>(() => { Dictionary dict = new Dictionary(); @@ -364,7 +394,7 @@ public class BomEntity : IEquatable // to craft a List "result" at run-time: Type listType = typeof(List<>); Type constructedListType = listType.MakeGenericType(type); - // Needed? var helper = Activator.CreateInstance(constructedListType); + // Would we want to stach a pre-created helper instance as Activator.CreateInstance(constructedListType) ? dict[type] = new BomEntityListReflection(); dict[type].genericType = constructedListType; @@ -372,8 +402,8 @@ public class BomEntity : IEquatable // Gotta use reflection for run-time evaluated type methods: dict[type].propCount = constructedListType.GetProperty("Count"); dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); - dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); - dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); + dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. @@ -382,7 +412,7 @@ public class BomEntity : IEquatable return dict; }) (); - public static Dictionary KnownBomEntityListMergeHelpers = + public static readonly Dictionary KnownBomEntityListMergeHelpers = new Func>(() => { Dictionary dict = new Dictionary(); @@ -402,14 +432,14 @@ public class BomEntity : IEquatable if (LType != null) { // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new [] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); if (methodMerge != null) { dict[type] = new BomEntityListMergeHelperReflection(); dict[type].genericType = constructedListHelperType; dict[type].methodMerge = methodMerge; dict[type].helperInstance = helper; - // Callers would return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + // Callers would return something like (List)methodMerge.Invoke(helper, new object[] {list1, list2}) } else { @@ -430,21 +460,23 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - public static Dictionary KnownTypeSerializers = + public static readonly Dictionary KnownTypeSerializers = new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); var methodDefault = jserClassType.GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { var method = jserClassType.GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null && method != methodDefault) + { dict[type] = method; + } } return dict; }) (); @@ -454,7 +486,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeEquals = + public static readonly Dictionary KnownTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -462,27 +494,31 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); - public static Dictionary KnownDefaultEquals = + public static readonly Dictionary KnownDefaultEquals = new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method == null) + { dict[type] = methodDefault; + } } return dict; }) (); @@ -490,7 +526,7 @@ public class BomEntity : IEquatable // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. - public static Dictionary KnownOtherTypeEquals = + public static readonly Dictionary KnownOtherTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -502,7 +538,7 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); dict[type] = method; } return dict; @@ -513,7 +549,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeEquivalent = + public static readonly Dictionary KnownTypeEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); @@ -521,27 +557,31 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); - public static Dictionary KnownDefaultEquivalent = + public static readonly Dictionary KnownDefaultEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method == null) + { dict[type] = methodDefault; + } } return dict; }) (); @@ -551,7 +591,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeMergeWith = + public static readonly Dictionary KnownTypeMergeWith = new Func>(() => { Dictionary dict = new Dictionary(); @@ -559,16 +599,18 @@ public class BomEntity : IEquatable { var method = type.GetMethod("MergeWith", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); protected BomEntity() { - // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); + // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") } /// @@ -600,20 +642,33 @@ internal string SerializeEntity() /// should be by default aware and capable of ultimately serializing the properties /// relevant to each derived class. /// - /// Another BomEntity-derived object of same type + /// Another BomEntity-derived object of same type /// True if two objects are deemed equal - public bool Equals(BomEntity other) + public bool Equals(BomEntity obj) { Type thisType = this.GetType(); if (KnownTypeEquals.TryGetValue(thisType, out var methodEquals)) { - return (bool)methodEquals.Invoke(this, new object[] {other}); + return (bool)methodEquals.Invoke(this, new object[] {obj}); } - if (other is null || thisType != other.GetType()) return false; - return this.SerializeEntity() == other.SerializeEntity(); + if (obj is null || thisType != obj.GetType()) + { + return false; + } + return this.SerializeEntity() == obj.SerializeEntity(); } - + + // Needed by IEquatable contract + public override bool Equals(Object obj) + { + if (obj is null || !(obj is BomEntity)) + { + return false; + } + return this.Equals((BomEntity)obj); + } + public override int GetHashCode() { return this.SerializeEntity().GetHashCode(); @@ -626,23 +681,23 @@ public override int GetHashCode() /// are not) by defining an implementation tailored to that derived type /// as the argument, or keep this default where equiality is equivalence. /// - /// Another object of same type + /// Another object of same type /// True if two data objects are considered to represent /// the same real-life entity, False otherwise. - public bool Equivalent(BomEntity other) + public bool Equivalent(BomEntity obj) { Type thisType = this.GetType(); if (KnownTypeEquivalent.TryGetValue(thisType, out var methodEquivalent)) { - // Note we do not check for null/type of "other" at this point + // Note we do not check for null/type of "obj" at this point // since the derived classes define the logic of equivalence // (possibly to other entity subtypes as well). - return (bool)methodEquivalent.Invoke(this, new object[] {other}); + return (bool)methodEquivalent.Invoke(this, new object[] {obj}); } // Note that here a default Equivalent() may call into custom Equals(), // so the similar null/type sanity shecks are still relevant. - return (!(other is null) && (thisType == other.GetType()) && this.Equals(other)); + return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } /// @@ -653,7 +708,7 @@ public bool Equivalent(BomEntity other) /// Treats a null "other" object as a success (it is effectively a /// no-op merge, which keeps "this" object as is). /// - /// Another object of same type whose additional + /// Another object of same type whose additional /// non-conflicting data we try to squash into this object. /// True if merge was successful, False if it these objects /// are not equivalent, or throws if merge can not be done (including @@ -661,24 +716,33 @@ public bool Equivalent(BomEntity other) /// /// Source data problem: two entities with conflicting information /// Caller error: somehow merging different entity types - public bool MergeWith(BomEntity other) + public bool MergeWith(BomEntity obj) { - if (other is null) return true; - if (this.GetType() != other.GetType()) + if (obj is null) + { + return true; + } + if (this.GetType() != obj.GetType()) { // Note: potentially descendent classes can catch this // to adapt their behavior... if some two different // classes would ever describe something comparable // in real life. - throw new BomEntityIncompatibleException(this.GetType(), other.GetType()); + throw new BomEntityIncompatibleException(this.GetType(), obj.GetType()); } - if (this.Equals(other)) return true; + if (this.Equals(obj)) + { + return true; + } // Avoid calling Equals => serializer twice for no gain // (default equivalence is equality): if (KnownTypeEquivalent.TryGetValue(this.GetType(), out var methodEquivalent)) { - if (!this.Equivalent(other)) return false; + if (!this.Equivalent(obj)) + { + return false; + } // else fall through to exception below } else diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 74373c8d..55aecb09 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -119,7 +119,9 @@ public ComponentScope NonNullableScope get { if (Scope == null) + { return ComponentScope.Null; + } return Scope.Value; } set @@ -209,7 +211,9 @@ public bool Equivalent(Component obj) public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } try { @@ -226,7 +230,9 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + } } } return resBase; @@ -247,19 +253,23 @@ public bool MergeWith(Component obj) (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) ) { - // Objects seem equivalent according to critical arguments; + // Objects seem equivalent according to critical arguments => // merge the attribute values with help of reflection: if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + } PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; if (iDebugLevel >= 2) + { Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + } - // Use a temporary clone instead of mangling "this" object right away; - // note serialization seems to skip over "nonnullable" values in some cases + // Use a temporary clone instead of mangling "this" object right away. + // Note: serialization seems to skip over "nonnullable" values in some cases. Component tmp = new Component(); /* This copier fails due to copy of "non-null" fields which may be null: - * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)) */ foreach (PropertyInfo property in properties) { @@ -267,11 +277,11 @@ public bool MergeWith(Component obj) // Avoid spurious "modified=false" in merged JSON // Also skip helpers, care about real values if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(this.Modified.HasValue)) { - // Can not set R/O prop: ### tmp.Modified.HasValue = false; + // Can not set R/O prop: ### tmp.Modified.HasValue = false continue; } if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(this.Scope.HasValue)) { - // Can not set R/O prop: ### tmp.Scope.HasValue = false; + // Can not set R/O prop: ### tmp.Scope.HasValue = false continue; } property.SetValue(tmp, property.GetValue(this, null)); @@ -284,7 +294,9 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); + } switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -295,7 +307,9 @@ public bool MergeWith(Component obj) // NOTE: Intentionally not matching 'Scope' helper // Not nullable! Quickly keep un-set if applicable. if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) + { continue; + } ComponentScope tmpItem; try @@ -328,7 +342,9 @@ public bool MergeWith(Component obj) } if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + } // Since CycloneDX spec v1.0 up to at least v1.4, // an absent value "SHOULD" be treated as "required" @@ -338,7 +354,9 @@ public bool MergeWith(Component obj) if (tmpItem == ComponentScope.Null && objItem == ComponentScope.Null) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: keep unspecified explicitly"); + } continue; } @@ -346,7 +364,9 @@ public bool MergeWith(Component obj) { property.SetValue(tmp, ComponentScope.Optional); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: keep 'Optional'"); + } continue; } @@ -354,7 +374,9 @@ public bool MergeWith(Component obj) // keep absent=>required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); + } continue; } @@ -369,7 +391,9 @@ public bool MergeWith(Component obj) // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); + } continue; } @@ -379,7 +403,9 @@ public bool MergeWith(Component obj) // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); + } mergedOk = false; } break; @@ -388,14 +414,22 @@ public bool MergeWith(Component obj) { // Not nullable! Keep un-set if applicable. if (!obj.Modified.HasValue) + { continue; + } + bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + } + if (objItem) + { property.SetValue(tmp, true); + } } break; @@ -407,7 +441,9 @@ public bool MergeWith(Component obj) if (propValTmp == null && propValObj == null) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); + } continue; } @@ -425,7 +461,9 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); + } propCount = LType.GetProperty("Count"); methodGetItem = LType.GetMethod("get_Item"); methodAdd = LType.GetMethod("Add"); @@ -434,7 +472,9 @@ public bool MergeWith(Component obj) if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + } mergedOk = false; continue; } @@ -442,7 +482,9 @@ public bool MergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + } if (propValObj == null || propValObjCount < 1) { @@ -459,7 +501,7 @@ public bool MergeWith(Component obj) if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) methodMergeWith = null; } @@ -477,9 +519,11 @@ public bool MergeWith(Component obj) } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + methodEquals = TType.GetMethod("Equals", 0, new [] { TType }); if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } } @@ -488,7 +532,9 @@ public bool MergeWith(Component obj) { var objItem = methodGetItem.Invoke(propValObj, new object[] { o }); if (objItem is null) + { continue; + } bool listHit = false; for (int t = 0; t < propValTmpCount && !listHit; t++) @@ -505,8 +551,10 @@ public bool MergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): try methodEquals()"); - propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); + } + propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new [] {objItem}); propsSeemEqualLearned = true; } } @@ -514,7 +562,9 @@ public bool MergeWith(Component obj) { // no-op if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + } } if (propsSeemEqual || !propsSeemEqualLearned) @@ -528,21 +578,29 @@ public bool MergeWith(Component obj) try { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); - if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) + } + if (!((bool)methodMergeWith.Invoke(tmpItem, new [] {objItem}))) + { mergedOk = false; + } } catch (System.Exception exc) { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + } mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { if (iDebugLevel >= 7) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + } } } // else: tmpitem considered not equal, should be added } @@ -550,7 +608,7 @@ public bool MergeWith(Component obj) if (!listHit) { - methodAdd.Invoke(propValTmp, new object[] {objItem}); + methodAdd.Invoke(propValTmp, new [] {objItem}); propValTmpCount = (int)propCount.GetValue(propValTmp, null); } } @@ -584,12 +642,16 @@ public bool MergeWith(Component obj) // followed by '{ComponentScope NonNullableScope}' // which we specially handle above if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); + } continue; } if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); + } var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -618,9 +680,11 @@ public bool MergeWith(Component obj) } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + methodEquals = TType.GetMethod("Equals", 0, new [] { TType }); if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } } @@ -635,8 +699,10 @@ public bool MergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): try methodEquals()"); - propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); + } + propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new [] {propValObj}); propsSeemEqualLearned = true; } } @@ -644,7 +710,9 @@ public bool MergeWith(Component obj) { // no-op if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + } } try @@ -653,7 +721,9 @@ public bool MergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); + } propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -669,7 +739,9 @@ public bool MergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); + } propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -682,13 +754,15 @@ public bool MergeWith(Component obj) if (!propsSeemEqual) { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): items say they are not equal"); + } } if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) methodMergeWith = null; } @@ -696,16 +770,22 @@ public bool MergeWith(Component obj) { try { - if (!((bool)methodMergeWith.Invoke(propValTmp, new object[] {propValObj}))) + if (!((bool)methodMergeWith.Invoke(propValTmp, new [] {propValObj}))) + { mergedOk = false; + } } catch (System.Exception exc) { // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) + { continue; + } if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + } mergedOk = false; } } @@ -713,9 +793,13 @@ public bool MergeWith(Component obj) { // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) + { continue; + } if (iDebugLevel >= 7) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + } mergedOk = false; } } @@ -730,11 +814,11 @@ public bool MergeWith(Component obj) // Avoid spurious "modified=false" in merged JSON // Also skip helpers, care about real values if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(tmp.Modified.HasValue)) { - // Can not set R/O prop: ### this.Modified.HasValue = false; + // Can not set R/O prop: ### this.Modified.HasValue = false continue; } if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(tmp.Scope.HasValue)) { - // Can not set R/O prop: ### this.Scope.HasValue = false; + // Can not set R/O prop: ### this.Scope.HasValue = false continue; } property.SetValue(this, property.GetValue(tmp, null)); @@ -742,13 +826,17 @@ public bool MergeWith(Component obj) } if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + } return mergedOk; } else { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related upon second look"); + } } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Core/Models/Hash.cs b/src/CycloneDX.Core/Models/Hash.cs index 4f54fa6f..1bf4d761 100644 --- a/src/CycloneDX.Core/Models/Hash.cs +++ b/src/CycloneDX.Core/Models/Hash.cs @@ -63,32 +63,32 @@ public enum HashAlgorithm [ProtoMember(2)] public string Content { get; set; } - public bool Equivalent(Hash other) + public bool Equivalent(Hash obj) { - return (!(other is null) && this.Alg == other.Alg); + return (!(obj is null) && this.Alg == obj.Alg); } - public bool MergeWith(Hash other) + public bool MergeWith(Hash obj) { try { // Basic checks for null, type compatibility, // equality and non-equivalence; throws for // the hard stuff to implement in the catch: - return base.MergeWith(other); + return base.MergeWith(obj); } catch (BomEntityConflictException) { // Note: Alg is non-nullable so no check for that - if (this.Content is null && !(other.Content is null)) + if (this.Content is null && !(obj.Content is null)) { - this.Content = other.Content; + this.Content = obj.Content; return true; } - if (this.Content != other.Content) + if (this.Content != obj.Content) { - throw new BomEntityConflictException("Two Hash objects with same Alg='${this.Alg}' and different Content: '${this.Content}' vs. '${other.Content}'"); + throw new BomEntityConflictException($"Two Hash objects with same Alg='{this.Alg}' and different Content: '{this.Content}' vs. '{obj.Content}'"); } // All known properties merged or were equal/equivalent diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index c35008d6..fac1387d 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -58,7 +58,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } var result = new Bom(); result.Metadata = new Metadata @@ -95,21 +97,27 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy // twice should be effectively no-op); try to merge instead: if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); + } if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) { // bom1's entry is not null and seems equivalent to bom2's: - if (iDebugLevel >= 1) + if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); + } result.Metadata.Component = bom1.Metadata.Component; result.Metadata.Component.MergeWith(bom2.Metadata.Component); } else { - if (iDebugLevel >= 1) + if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); + } result.Components.Add(bom2.Metadata.Component); } } @@ -256,7 +264,10 @@ public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) if (bomSubject != null) { - if (bomSubject.BomRef is null) bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); + if (bomSubject.BomRef is null) + { + bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); + } result.Metadata.Component = bomSubject; result.Metadata.Tools = new List(); } @@ -365,23 +376,38 @@ bom.SerialNumber is null public static Bom CleanupMetadataComponent(Bom result) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); + } + if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) { if (iDebugLevel >= 2) + { Console.WriteLine($"MERGE-CLEANUP: Searching in list"); + } foreach (Component component in result.Components) { if (iDebugLevel >= 2) + { Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); - if (component is null) continue; // should not happen + } + if (component is null) + { + // should not happen, but... + continue; + } if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) { if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); + } result.Metadata.Component.MergeWith(component); result.Components.Remove(component); return result; @@ -390,20 +416,49 @@ public static Bom CleanupMetadataComponent(Bom result) } if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: NO HITS"); + } return result; } public static Bom CleanupEmptyLists(Bom result) { // cleanup empty top level elements - if (result.Metadata?.Tools?.Count == 0) result.Metadata.Tools = null; - if (result.Components?.Count == 0) result.Components = null; - if (result.Services?.Count == 0) result.Services = null; - if (result.ExternalReferences?.Count == 0) result.ExternalReferences = null; - if (result.Dependencies?.Count == 0) result.Dependencies = null; - if (result.Compositions?.Count == 0) result.Compositions = null; - if (result.Vulnerabilities?.Count == 0) result.Vulnerabilities = null; + if (result.Metadata?.Tools?.Count == 0) + { + result.Metadata.Tools = null; + } + + if (result.Components?.Count == 0) + { + result.Components = null; + } + + if (result.Services?.Count == 0) + { + result.Services = null; + } + + if (result.ExternalReferences?.Count == 0) + { + result.ExternalReferences = null; + } + + if (result.Dependencies?.Count == 0) + { + result.Dependencies = null; + } + + if (result.Compositions?.Count == 0) + { + result.Compositions = null; + } + + if (result.Vulnerabilities?.Count == 0) + { + result.Vulnerabilities = null; + } return result; } @@ -435,9 +490,11 @@ private static void NamespaceComponentBomRefs(Component topComponent) var currentComponent = components.Pop(); if (currentComponent.Components != null) - foreach (var subComponent in currentComponent.Components) { - components.Push(subComponent); + foreach (var subComponent in currentComponent.Components) + { + components.Push(subComponent); + } } currentComponent.BomRef = NamespacedBomRef(topComponent, currentComponent.BomRef); @@ -473,9 +530,11 @@ private static void NamespaceDependencyBomRefs(string bomRefNamespace, List Date: Fri, 11 Aug 2023 03:46:11 +0200 Subject: [PATCH 084/285] BomEntity.cs: address CI complaints (protect prepared public lists and dicts as immutable) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 67 +++++++++++++------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index bdc5f58b..25f45aed 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -354,8 +355,8 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - public static readonly List KnownEntityTypes = - new Func>(() => + public static readonly ImmutableList KnownEntityTypes = + new Func>(() => { List derived_types = new List(); foreach (var domain_assembly in AppDomain.CurrentDomain.GetAssemblies()) @@ -365,7 +366,7 @@ public class BomEntity : IEquatable derived_types.AddRange(assembly_types); } - return derived_types; + return ImmutableList.Create(derived_types.ToArray()); }) (); /// @@ -373,19 +374,19 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownEntityTypeProperties = - new Func>(() => + public static readonly ImmutableDictionary KnownEntityTypeProperties = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { dict[type] = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownEntityTypeLists = - new Func>(() => + public static readonly ImmutableDictionary KnownEntityTypeLists = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -409,11 +410,11 @@ public class BomEntity : IEquatable // TODO: Separate dict?.. dict[constructedListType] = dict[type]; } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownBomEntityListMergeHelpers = - new Func>(() => + public static readonly ImmutableDictionary KnownBomEntityListMergeHelpers = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -452,7 +453,7 @@ public class BomEntity : IEquatable throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a List class definition"); } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -460,8 +461,8 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeSerializers = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeSerializers = + new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); var methodDefault = jserClassType.GetMethod("Serialize", @@ -478,7 +479,7 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -486,8 +487,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeEquals = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -500,11 +501,11 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownDefaultEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownDefaultEquals = + new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equals", @@ -520,14 +521,14 @@ public class BomEntity : IEquatable dict[type] = methodDefault; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. - public static readonly Dictionary KnownOtherTypeEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownOtherTypeEquals = + new Func>(() => { Dictionary dict = new Dictionary(); var listMore = new List(); @@ -541,7 +542,7 @@ public class BomEntity : IEquatable new [] { type }); dict[type] = method; } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -549,8 +550,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeEquivalent = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeEquivalent = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -563,11 +564,11 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownDefaultEquivalent = - new Func>(() => + public static readonly ImmutableDictionary KnownDefaultEquivalent = + new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equivalent", @@ -583,7 +584,7 @@ public class BomEntity : IEquatable dict[type] = methodDefault; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -591,8 +592,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeMergeWith = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeMergeWith = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -605,7 +606,7 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); protected BomEntity() From 618a90caf4cfcb30bde16c432460c1db599f1b0e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 03:46:35 +0200 Subject: [PATCH 085/285] BomEntity.cs: address CI complaints (protect helper class public fields with getters/setters) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 25f45aed..454da3e4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -80,12 +80,12 @@ public class BomEntityListMergeHelperStrategy /// deduplicating based on that (goes faster but /// may cause data structure not conforming to spec) /// - public bool useBomEntityMerge; + public bool useBomEntityMerge { get; set; } /// /// CycloneDX spec version. /// - public SpecificationVersion specificationVersion; + public SpecificationVersion specificationVersion { get; set; } /// /// Return reasonable default strategy settings. @@ -320,18 +320,18 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat public class BomEntityListReflection { - public Type genericType; - public PropertyInfo propCount; - public MethodInfo methodAdd; - public MethodInfo methodAddRange; - public MethodInfo methodGetItem; + public Type genericType { get; set; } + public PropertyInfo propCount { get; set; } + public MethodInfo methodAdd { get; set; } + public MethodInfo methodAddRange { get; set; } + public MethodInfo methodGetItem { get; set; } } public class BomEntityListMergeHelperReflection { - public Type genericType; - public MethodInfo methodMerge; - public Object helperInstance; + public Type genericType { get; set; } + public MethodInfo methodMerge { get; set; } + public Object helperInstance { get; set; } } /// From d1938c30beff31c9410e83f29a679323e8d5e1d6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 09:11:19 +0200 Subject: [PATCH 086/285] ListMergeHelper: update doc summary of the class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 553f6067..c110aa9b 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -27,9 +27,14 @@ namespace CycloneDX /// Allows to merge generic lists with items of specified types /// (by default essentially adding entries which are not present /// yet according to List.Contains() method), and calls special - /// logic for lists of BomEntry types. + /// logic for lists of BomEntry types.
+ /// /// Used in CycloneDX.Utils various Merge implementations as well - /// as in CycloneDX.Core BomEntity-derived classes' MergeWith(). + /// as in CycloneDX.Core BomEntity-derived classes' MergeWith().
+ /// + /// Does not modify original lists and returns a new instance + /// with merged data. One exception is if one of the inputs is + /// null or empty - then the other object is returned. ///
/// Type of listed entries public class ListMergeHelper From aff0ea5de42eda64f35872ab4882a1a82a773846 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 14 Aug 2023 20:44:18 +0200 Subject: [PATCH 087/285] Validator.cs: document addDictList() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 0d2cc76b..5f4acc09 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,6 +166,15 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } + /// + /// Merge two dictionaries whose values are lists of JsonElements, + /// adding all entries from list in dict2 for the same key as in + /// dict1 (or adds a new entry for a new key). Manipulates a COPY + /// of dict1, then returns this copy. + /// + /// Dict with lists as values + /// Dict with lists as values + /// Copy of dict1+dict2 private static Dictionary> addDictList( Dictionary> dict1, Dictionary> dict2) From 11b62da74d2837aeeb249289fc5f5170c0d7c443 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 14 Aug 2023 20:56:56 +0200 Subject: [PATCH 088/285] Validator.cs: check that for each "ref" pointer in the document, a target "bom-ref" is defined Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 5f4acc09..68b02382 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -301,6 +301,16 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } } + + // Check that if we "ref" something (from dependencies, etc.) + // the corresponding "bom-ref" exists in this document: + List bomRefsList = new List(bomRefs.Keys); + Dictionary> useRefs = findNamedElements(jsonDocument.RootElement, "ref"); + foreach (KeyValuePair> KVP in useRefs) { + if (KVP.Value != null && KVP.Value.Count > 0 && !(bomRefsList.Contains(KVP.Key))) { + validationMessages.Add($"'ref' value of {KVP.Key} was used in {KVP.Value.Count} place(s); expected a 'bom-ref' defined for it, but there was none"); + } + } } else { From cde95ef174f7d3b8d55e3db07a9cdd80c79b8645 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 22 Jul 2023 02:11:31 +0200 Subject: [PATCH 089/285] Merge.cs, Component.cs: add support for optional mergeWith() method to process the complex object properties to squash two similar items together into one better informed entity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 153 ++++++++++++++++++++++++- src/CycloneDX.Utils/Merge.cs | 50 +++++++- 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 8fa0edba..48ec8ea7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -18,6 +18,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -201,10 +203,159 @@ public bool Equals(Component obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); } - + public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } + + public bool mergeWith(Component obj) + { + if (this.Equals(obj)) + // Contents are identical, nothing to do: + return true; + + if ( + (this.BomRef != null && BomRef.Equals(obj.BomRef)) || + (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) + ) { + // Objects seem equivalent according to critical arguments; + // merge the attribute values with help of reflection: + PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance); + + // Use a temporary clone instead of mangling "this" object right away: + Component tmp = new Component(); + tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + bool mergedOk = true; + + foreach (PropertyInfo property in properties) + { + switch (property.PropertyType) + { + case Type _ when property.PropertyType == typeof(ComponentScope): + { + // Not nullable! + ComponentScope tmpItem = (ComponentScope)property.GetValue(tmp, null); + ComponentScope objItem = (ComponentScope)property.GetValue(obj, null); + if (tmpItem == objItem) + { + continue; + } + else + { + // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) + { + if (objItem != ComponentScope.Excluded) + // keep absent==required; upgrade optional objItem to value of tmp + property.SetValue(tmp, ComponentScope.Required); + continue; + } + + if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + { + if (tmpItem != ComponentScope.Excluded) + // set required; upgrade optional tmpItem (if such) + property.SetValue(tmp, ComponentScope.Required); + continue; + } + } + + // Here throw some exception or trigger creation of new object with a + // new bom-ref - and a new identification in the original document to + // avoid conflicts; be sure then to check for other entries that have + // everything same except bom-ref (match the expected new pattern)?.. + mergedOk = false; + } + break; + + case Type _ when property.PropertyType == typeof(List): + { + foreach (var objItem in ((List)(property.GetValue(obj, null)))) + { + if (objItem is null) + continue; + + bool listHit = false; + foreach (var tmpItem in ((List)(property.GetValue(tmp, null)))) + { + if (tmpItem != null && tmpItem == objItem) + { + listHit = true; + var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + mergedOk = false; + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } // else: no method, just trust equality - avoid "Add" to merge below + else + { + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: no such method"); + } + } + } + + if (!listHit) + { + (((List)property.GetValue(tmp, null))).Add(objItem); + } + } + } + break; + + default: + { + var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + mergedOk = false; + } + catch (System.Exception exc) + { + // That property's class lacks a mergeWith(), gotta trust the equality: + if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + continue; + Console.WriteLine($"FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } + else + { + // That property's class lacks a mergeWith(), gotta trust the equality: + Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + continue; + mergedOk = false; + } + } + break; + } + } + + if (mergedOk) { + // No failures, only now update the current object: + foreach (PropertyInfo property in properties) + { + property.SetValue(this, property.GetValue(tmp, null)); + } + } + + return mergedOk; + } + + // Merge was not applicable or otherwise did not succeed + return false; + } } } \ No newline at end of file diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 372649b1..3c717ad3 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,16 +27,58 @@ class ListMergeHelper { public List Merge(List list1, List list2) { + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); if (list1 is null) return list2; if (list2 is null) return list1; - var result = new List(list1); + List result = new List(list1); - foreach (var item in list2) + foreach (var item2 in list2) { - if (!(result.Contains(item))) + bool isContained = false; + for (int i=0; i < result.Count; i++) { - result.Add(item); + T item1 = result[i]; + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there: + var method = item1.GetType().GetMethod("mergeWith"); + if (method != null) + { + try + { + if (((bool)method.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; // items deemed equivalent + } + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } // else: That class lacks a mergeWith(), gotta trust the equality + else + { + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1.Equals(item2)) { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + } + } + + if (!isContained) + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + else + { + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } } From 4545835766dde3bfe139fa2083152d1fdcbd43bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:34:25 +0200 Subject: [PATCH 090/285] Merge.cs: refactor with GetMethod() called once and using type-specific Equals() implementation, fix remaining duplicates Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 63 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3c717ad3..1ca2b1a9 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -33,25 +33,30 @@ public List Merge(List list1, List list2) List result = new List(list1); + var TType = ((T)list2[0]).GetType(); + var methodMergeWith = TType.GetMethod("mergeWith"); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + foreach (var item2 in list2) { bool isContained = false; + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); for (int i=0; i < result.Count; i++) { + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to // IEquatable<>.Equals() checks defined in respective // classes), if there is a method defined there: - var method = item1.GetType().GetMethod("mergeWith"); - if (method != null) + if (methodMergeWith != null) { try { - if (((bool)method.Invoke(item1, new object[] {item2}))) + if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) { isContained = true; - break; // items deemed equivalent + break; // item2 merged into result[item1] or already equal to it } } catch (System.Exception exc) @@ -61,11 +66,53 @@ public List Merge(List list1, List list2) } // else: That class lacks a mergeWith(), gotta trust the equality else { - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1.Equals(item2)) { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1 is IEquatable) + { + if (methodEquals != null) + { + try + { + Console.WriteLine($"LIST-MERGE: try methodEquals()"); + if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; + } + } + catch (System.Exception exc) + { + Console.WriteLine($"SKIP MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } + + if (item1.Equals(item2)) + { + // Fall back to generic equality check which may be useless + Console.WriteLine($"SKIP MERGE: items say they are equal"); + isContained = true; + break; // items deemed equivalent + } + + Console.WriteLine($"MERGE: items say they are not equal"); + } + else + { + Console.WriteLine($"MERGE: items are not IEquatable"); + } +/* + else + { + if (item1 is CycloneDX.Models.Bom) + { + if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) + { + isContained = true; + break; // items deemed equivalent + } + } } +*/ } } From 8f7f53f01de5267895dcb72281fd2689bd71abb8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:44:08 +0200 Subject: [PATCH 091/285] Merge.cs: ListMerge: quiesce much of debug message noise Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 1ca2b1a9..f8a2fddd 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -40,10 +40,10 @@ public List Merge(List list1, List list2) foreach (var item2 in list2) { bool isContained = false; - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + /* Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); */ for (int i=0; i < result.Count; i++) { - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + /* Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); */ T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to @@ -66,14 +66,14 @@ public List Merge(List list1, List list2) } // else: That class lacks a mergeWith(), gotta trust the equality else { - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + /* Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); */ if (item1 is IEquatable) { if (methodEquals != null) { try { - Console.WriteLine($"LIST-MERGE: try methodEquals()"); + /* Console.WriteLine($"LIST-MERGE: try methodEquals()"); */ if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) { isContained = true; @@ -82,7 +82,7 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + /* Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ } } From 363f6f5e02cdffa808146e49b7dc570bff35963a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:58:11 +0200 Subject: [PATCH 092/285] Component.cs: try using ListMergeHelper Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 48ec8ea7..13266117 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -24,6 +24,8 @@ using System.Xml.Serialization; using ProtoBuf; +using CycloneDX.Utils.ListMergeHelper; + namespace CycloneDX.Models { [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] @@ -271,6 +273,25 @@ public bool mergeWith(Component obj) case Type _ when property.PropertyType == typeof(List): { + var listTmp = ((List)(property.GetValue(tmp, null))); + var listObj = ((List)(property.GetValue(obj, null))); +/* + if (listObj == null || listObj.Count == 0) + { + // Keep whatever "this" version of the list as the only one relevant + break; + } + + if (listTmp == null || listTmp.Count == 0) + { + // Keep whatever "other" version of the list as the only one relevant + property.SetValue(tmp, listObj); + break; + } +*/ + var propertyMerger = new ListMergeHelper(); + property.SetValue(tmp, propertyMerger.Merge(listTmp, listObj)); +/* foreach (var objItem in ((List)(property.GetValue(obj, null)))) { if (objItem is null) @@ -308,6 +329,7 @@ public bool mergeWith(Component obj) (((List)property.GetValue(tmp, null))).Add(objItem); } } +*/ } break; From 4c5e93dad927e6553b0f5f33fd1b24a1eeda1d79 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 00:58:25 +0200 Subject: [PATCH 093/285] Revert "Component.cs: try using ListMergeHelper" This reverts commit 9239be06d819d1778ea415d59cf287711479ed45. Can't easily pull in Utils dependency. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 13266117..48ec8ea7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -24,8 +24,6 @@ using System.Xml.Serialization; using ProtoBuf; -using CycloneDX.Utils.ListMergeHelper; - namespace CycloneDX.Models { [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] @@ -273,25 +271,6 @@ public bool mergeWith(Component obj) case Type _ when property.PropertyType == typeof(List): { - var listTmp = ((List)(property.GetValue(tmp, null))); - var listObj = ((List)(property.GetValue(obj, null))); -/* - if (listObj == null || listObj.Count == 0) - { - // Keep whatever "this" version of the list as the only one relevant - break; - } - - if (listTmp == null || listTmp.Count == 0) - { - // Keep whatever "other" version of the list as the only one relevant - property.SetValue(tmp, listObj); - break; - } -*/ - var propertyMerger = new ListMergeHelper(); - property.SetValue(tmp, propertyMerger.Merge(listTmp, listObj)); -/* foreach (var objItem in ((List)(property.GetValue(obj, null)))) { if (objItem is null) @@ -329,7 +308,6 @@ public bool mergeWith(Component obj) (((List)property.GetValue(tmp, null))).Add(objItem); } } -*/ } break; From 0a6f7ee711704f8bdffadcca1894b16437517a4c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 04:30:18 +0200 Subject: [PATCH 094/285] Component.cs: experiment more for mergeWith() to actually act Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 271 +++++++++++++++++++++---- 1 file changed, 234 insertions(+), 37 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 48ec8ea7..04c9fff0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -212,8 +212,10 @@ public override int GetHashCode() public bool mergeWith(Component obj) { if (this.Equals(obj)) - // Contents are identical, nothing to do: + { + Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); return true; + } if ( (this.BomRef != null && BomRef.Equals(obj.BomRef)) || @@ -221,91 +223,200 @@ public bool mergeWith(Component obj) ) { // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: - PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance); + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); - // Use a temporary clone instead of mangling "this" object right away: + // Use a temporary clone instead of mangling "this" object right away; + // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); - tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + foreach (PropertyInfo property in properties) + { + try { + property.SetValue(tmp, property.GetValue(this, null)); + } catch (System.Exception) { + // no-op + } + } + /* tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ bool mergedOk = true; foreach (PropertyInfo property in properties) { + Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { + case Type _ when property.PropertyType == typeof(Nullable): + break; +/* + case Type _ when property.PropertyType == typeof(Nullable[]): + break; +*/ case Type _ when property.PropertyType == typeof(ComponentScope): { // Not nullable! - ComponentScope tmpItem = (ComponentScope)property.GetValue(tmp, null); - ComponentScope objItem = (ComponentScope)property.GetValue(obj, null); - if (tmpItem == objItem) + ComponentScope tmpItem; + try + { + tmpItem = (ComponentScope)property.GetValue(tmp, null); + } + catch (System.Exception) + { + // Unspecified => required per CycloneDX spec v1.4?.. + tmpItem = ComponentScope.Null; + } + + ComponentScope objItem; + try + { + objItem = (ComponentScope)property.GetValue(obj, null); + } + catch (System.Exception) + { + objItem = ComponentScope.Null; + } + + Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + + // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { + // keep absent==required; upgrade optional objItem + property.SetValue(tmp, ComponentScope.Required); + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); continue; } - else + + if ((tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional)) + { + // downgrade optional objItem to excluded + property.SetValue(tmp, ComponentScope.Excluded); + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + continue; + } + + +/* + if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) { - // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" - if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) + if (objItem != ComponentScope.Excluded) { - if (objItem != ComponentScope.Excluded) - // keep absent==required; upgrade optional objItem to value of tmp - property.SetValue(tmp, ComponentScope.Required); - continue; + // keep absent==required; upgrade optional objItem to value of tmp + property.SetValue(tmp, ComponentScope.Required); + continue; } + } - if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) + { + if (tmpItem != ComponentScope.Excluded) { - if (tmpItem != ComponentScope.Excluded) - // set required; upgrade optional tmpItem (if such) - property.SetValue(tmp, ComponentScope.Required); - continue; + // set required; upgrade optional tmpItem (if such) + property.SetValue(tmp, ComponentScope.Required); + continue; } } +*/ // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. + Console.WriteLine($"Component.mergeWith(): can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; - case Type _ when property.PropertyType == typeof(List): + case Type _ when (property.Name == "NonNullableModified"): + { + // Not nullable! + bool tmpItem = (bool)property.GetValue(tmp, null); + bool objItem = (bool)property.GetValue(obj, null); + + Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + if (objItem) + property.SetValue(tmp, true); + } + break; + + case Type _ when (property.PropertyType == typeof(List) || property.PropertyType.ToString().StartsWith("System.Collections.Generic.List")): { - foreach (var objItem in ((List)(property.GetValue(obj, null)))) + // https://www.experts-exchange.com/questions/22600200/Traverse-generic-List-using-C-Reflection.html + var propValTmp = property.GetValue(tmp); + var propValObj = property.GetValue(obj); + if (propValTmp == null && propValObj == null) + { + Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + continue; + } + + var LType = (propValTmp == null ? propValObj.GetType() : propValTmp.GetType()); + var propCount = LType.GetProperty("Count"); + var methodGetItem = LType.GetMethod("get_Item"); + var methodAdd = LType.GetMethod("Add"); + if (methodGetItem == null || propCount == null || methodAdd == null) + { + Console.WriteLine($"Component.mergeWith(): is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + mergedOk = false; + continue; + } + + int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); + int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); + Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + + if (propValObj == null || propValObjCount == 0 || propValObjCount == null) { + continue; + } + + if (propValTmp == null || propValTmpCount == 0 || propValTmpCount == null) + { + property.SetValue(tmp, propValObj); + continue; + } + + var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); + var methodMergeWith = TType.GetMethod("mergeWith"); + + for (int o = 0; o < propValObjCount; o++) + { + var objItem = methodGetItem.Invoke(propValObj, new object[] { o }); if (objItem is null) continue; bool listHit = false; - foreach (var tmpItem in ((List)(property.GetValue(tmp, null)))) + for (int t = 0; t < propValTmpCount; t++) { - if (tmpItem != null && tmpItem == objItem) + var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); + if (tmpItem != null) { listHit = true; - var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); - if (method != null) + if (methodMergeWith != null) { try { - if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem.ToString()} and {objItem.ToString()}: no such method"); + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); */ } - } + } // else: tmpitem considered not equal, should be added } if (!listHit) { - (((List)property.GetValue(tmp, null))).Add(objItem); + methodAdd.Invoke(propValTmp, new object[] {objItem}); + propValTmpCount = (int)propCount.GetValue(propValTmp, null); } } } @@ -313,29 +424,110 @@ public bool mergeWith(Component obj) default: { - var method = property.GetValue(tmp, null).GetType().GetMethod("mergeWith"); - if (method != null) + if ( + /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ + property.PropertyType.ToString().StartsWith("System.Nullable") + ) + { + // e.g. 'Scope' helper + // followed by 'Scope' + // which we specially handle above + Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + continue; + } + + Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + var propValTmp = property.GetValue(tmp, null); + var propValObj = property.GetValue(obj, null); + if (propValObj == null) + { + continue; + } + + if (propValTmp == null) + { + property.SetValue(tmp, propValObj); + continue; + } + + var TType = propValTmp.GetType(); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + bool propsSeemEqual = false; + bool propsSeemEqualLearned = false; + + try + { + if (methodEquals != null) + { + /* Console.WriteLine($"Component.mergeWith(): try methodEquals()"); */ + propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + /* Console.WriteLine($"Component.mergeWith(): can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + } + + try + { + if (!propsSeemEqualLearned) + { + // Fall back to generic equality check which may be useless + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + propsSeemEqual = propValTmp.Equals(propValObj); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + } + + try + { + if (!propsSeemEqualLearned) + { + // Fall back to generic equality check which may be useless + /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + propsSeemEqual = (propValTmp == propValObj); + propsSeemEqualLearned = true; + } + } + catch (System.Exception exc) + { + // no-op + } + + if (!propsSeemEqual) + { + Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + } + + var methodMergeWith = TType.GetMethod("mergeWith"); + if (methodMergeWith != null) { try { - if (!((bool)method.Invoke(property.GetValue(tmp, null), new object[] {property.GetValue(obj, null)}))) + if (!((bool)methodMergeWith.Invoke(propValTmp, new object[] {propValObj}))) mergedOk = false; } catch (System.Exception exc) { // That property's class lacks a mergeWith(), gotta trust the equality: - if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + if (propsSeemEqual) continue; - Console.WriteLine($"FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } else { // That property's class lacks a mergeWith(), gotta trust the equality: - Console.WriteLine($"SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); - if (property.GetValue(tmp, null) == property.GetValue(obj, null)) + if (propsSeemEqual) continue; + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -351,8 +543,13 @@ public bool mergeWith(Component obj) } } + Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } + else + { + Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + } // Merge was not applicable or otherwise did not succeed return false; From 37adbf7e612d5c8af91d867e4fbcb8d23d38e44d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:20:00 +0200 Subject: [PATCH 095/285] Merge.cs, Component.cs: relegate debug trace printing to CYCLONEDX_DEBUG_MERGE envvar Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 81 +++++++++++++++++--------- src/CycloneDX.Utils/Merge.cs | 39 +++++++++---- 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 04c9fff0..3c9373b7 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -211,9 +211,13 @@ public override int GetHashCode() public bool mergeWith(Component obj) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + if (this.Equals(obj)) { - Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); return true; } @@ -223,9 +227,11 @@ public bool mergeWith(Component obj) ) { // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases @@ -243,7 +249,8 @@ public bool mergeWith(Component obj) foreach (PropertyInfo property in properties) { - Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); + if (iDebugLevel >= 2) + Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -276,14 +283,16 @@ public bool mergeWith(Component obj) objItem = ComponentScope.Null; } - Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); continue; } @@ -291,7 +300,8 @@ public bool mergeWith(Component obj) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -322,7 +332,8 @@ public bool mergeWith(Component obj) // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. - Console.WriteLine($"Component.mergeWith(): can not merge two bom-refs with scope excluded and required"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; @@ -333,7 +344,8 @@ public bool mergeWith(Component obj) bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); - Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); } @@ -346,7 +358,8 @@ public bool mergeWith(Component obj) var propValObj = property.GetValue(obj); if (propValTmp == null && propValObj == null) { - Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -356,14 +369,16 @@ public bool mergeWith(Component obj) var methodAdd = LType.GetMethod("Add"); if (methodGetItem == null || propCount == null || methodAdd == null) { - Console.WriteLine($"Component.mergeWith(): is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); mergedOk = false; continue; } int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); - Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount == 0 || propValObjCount == null) { @@ -396,19 +411,22 @@ public bool mergeWith(Component obj) { try { - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); */ + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added } @@ -432,11 +450,13 @@ public bool mergeWith(Component obj) // e.g. 'Scope' helper // followed by 'Scope' // which we specially handle above - Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); continue; } - Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + if (iDebugLevel >= 3) + Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -459,7 +479,8 @@ public bool mergeWith(Component obj) { if (methodEquals != null) { - /* Console.WriteLine($"Component.mergeWith(): try methodEquals()"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; } @@ -467,7 +488,8 @@ public bool mergeWith(Component obj) catch (System.Exception exc) { // no-op - /* Console.WriteLine($"Component.mergeWith(): can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } try @@ -475,7 +497,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -490,7 +513,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - /* Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); */ + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -502,7 +526,8 @@ public bool mergeWith(Component obj) if (!propsSeemEqual) { - Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): items say they are not equal"); } var methodMergeWith = TType.GetMethod("mergeWith"); @@ -518,7 +543,8 @@ public bool mergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } @@ -527,7 +553,8 @@ public bool mergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -543,12 +570,14 @@ public bool mergeWith(Component obj) } } - Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } else { - Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index f8a2fddd..4728b21e 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,7 +27,11 @@ class ListMergeHelper { public List Merge(List list1, List list2) { - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); if (list1 is null) return list2; if (list2 is null) return list1; @@ -40,10 +44,12 @@ public List Merge(List list1, List list2) foreach (var item2 in list2) { bool isContained = false; - /* Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); */ + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); for (int i=0; i < result.Count; i++) { - /* Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); */ + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); T item1 = result[i]; // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to @@ -61,19 +67,22 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + if (iDebugLevel >= 1) + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); } } // else: That class lacks a mergeWith(), gotta trust the equality else { - /* Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); */ + if (iDebugLevel >= 6) + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); if (item1 is IEquatable) { if (methodEquals != null) { try { - /* Console.WriteLine($"LIST-MERGE: try methodEquals()"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: try methodEquals()"); if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) { isContained = true; @@ -82,23 +91,27 @@ public List Merge(List list1, List list2) } catch (System.Exception exc) { - /* Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); */ + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); } } if (item1.Equals(item2)) { // Fall back to generic equality check which may be useless - Console.WriteLine($"SKIP MERGE: items say they are equal"); + if (iDebugLevel >= 3) + Console.WriteLine($"SKIP MERGE: items say they are equal"); isContained = true; break; // items deemed equivalent } - Console.WriteLine($"MERGE: items say they are not equal"); + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items say they are not equal"); } else { - Console.WriteLine($"MERGE: items are not IEquatable"); + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items are not IEquatable"); } /* else @@ -120,12 +133,14 @@ public List Merge(List list1, List list2) { // Add new entry "as is" (new-ness is subject to // equality checks of respective classes): - Console.WriteLine($"WILL ADD: {item2.ToString()}"); + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); result.Add(item2); } else { - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } } From e91f16ff69371dbb94c7af74949d614822361db8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:20:45 +0200 Subject: [PATCH 096/285] Component.cs: address some compiler warnings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 3c9373b7..06ff6ad5 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -236,6 +236,9 @@ public bool mergeWith(Component obj) // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); + /* This fails due to copy of "non-null" fields which may be null: + * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + */ foreach (PropertyInfo property in properties) { try { @@ -244,7 +247,6 @@ public bool mergeWith(Component obj) // no-op } } - /* tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ bool mergedOk = true; foreach (PropertyInfo property in properties) @@ -328,6 +330,7 @@ public bool mergeWith(Component obj) } */ + // TODO: Having two same bom-refs is a syntax validation error... // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to // avoid conflicts; be sure then to check for other entries that have @@ -380,12 +383,12 @@ public bool mergeWith(Component obj) if (iDebugLevel >= 4) Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); - if (propValObj == null || propValObjCount == 0 || propValObjCount == null) + if (propValObj == null || propValObjCount < 1) { continue; } - if (propValTmp == null || propValTmpCount == 0 || propValTmpCount == null) + if (propValTmp == null || propValTmpCount < 1) { property.SetValue(tmp, propValObj); continue; @@ -503,7 +506,7 @@ public bool mergeWith(Component obj) propsSeemEqualLearned = true; } } - catch (System.Exception exc) + catch (System.Exception) { // no-op } @@ -519,7 +522,7 @@ public bool mergeWith(Component obj) propsSeemEqualLearned = true; } } - catch (System.Exception exc) + catch (System.Exception) { // no-op } From d67c3207e92be31369cc21408988d929eb7599c7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 27 Jul 2023 21:31:52 +0200 Subject: [PATCH 097/285] Component.cs: clean up handling of SCOPE merging Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 29 ++++---------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 06ff6ad5..7cb99833 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -298,8 +298,10 @@ public bool mergeWith(Component obj) continue; } - if ((tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional)) - { + if ( + (tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || + (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional) + ) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 3) @@ -307,29 +309,6 @@ public bool mergeWith(Component obj) continue; } - -/* - if (tmpItem == ComponentScope.Required || tmpItem == ComponentScope.Null) - { - if (objItem != ComponentScope.Excluded) - { - // keep absent==required; upgrade optional objItem to value of tmp - property.SetValue(tmp, ComponentScope.Required); - continue; - } - } - - if (objItem == ComponentScope.Required || objItem == ComponentScope.Null) - { - if (tmpItem != ComponentScope.Excluded) - { - // set required; upgrade optional tmpItem (if such) - property.SetValue(tmp, ComponentScope.Required); - continue; - } - } -*/ - // TODO: Having two same bom-refs is a syntax validation error... // Here throw some exception or trigger creation of new object with a // new bom-ref - and a new identification in the original document to From 86a35e356e3dcf9e08a851b81158153598e5d481 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 13:51:54 +0200 Subject: [PATCH 098/285] Merge.cs: always apply a new timestamp to freshly created "result" Bom object Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 4728b21e..51213fb6 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -166,15 +166,21 @@ public static partial class CycloneDXUtils public static Bom FlatMerge(Bom bom1, Bom bom2) { var result = new Bom(); + result.Metadata = new Metadata + { + // Note: we recurse into this method from other FlatMerge() implementations + // (e.g. mass-merge of a big list of Bom documents), so the resulting + // document gets a new timestamp every time. It is unique after all. + // Also note that a merge of "new Bom()" with a real Bom is also different + // from that original (serialNumber, timestamp, possible entry order, etc.) + Timestamp = DateTime.Now + }; var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { - result.Metadata = new Metadata - { - Tools = tools - }; + result.Metadata.Tools = tools; } var componentsMerger = new ListMergeHelper(); @@ -239,7 +245,11 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); - + + // Note: we were asked to "merge" and so we do, per principle of + // least surprise - even if there is just one entry in boms[] so + // we might be inclined to skip the loop. Resulting document WILL + // differ from such single original (serialNumber, timestamp...) foreach (var bom in boms) { result = FlatMerge(result, bom); @@ -267,8 +277,6 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) } result.Dependencies.Add(mainDependency); - - } return result; @@ -291,14 +299,16 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + result.Metadata = new Metadata + { + Timestamp = DateTime.Now + }; + if (bomSubject != null) { if (bomSubject.BomRef is null) bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); - result.Metadata = new Metadata - { - Component = bomSubject, - Tools = new List(), - }; + result.Metadata.Component = bomSubject; + result.Metadata.Tools = new List(); } result.Components = new List(); From 3f1161365f7ef9644c64335241e1965917262f92 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 13:53:11 +0200 Subject: [PATCH 099/285] Merge.cs: HierarchicalMerge(): be sure to have a non-null result.Metadata.Tools list before adding into it (might be AWOL if bomSubject==null at start) Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 51213fb6..73127990 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -332,6 +332,11 @@ bom.SerialNumber is null if (bom.Metadata?.Tools?.Count > 0) { + if (result.Metadata.Tools == null) + { + result.Metadata.Tools = new List(); + } + result.Metadata.Tools.AddRange(bom.Metadata.Tools); } From ec71bbee37495f79f9429b5c784e2c94931029a0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 15:51:38 +0200 Subject: [PATCH 100/285] Merge.cs: add logic to CleanupMetadataComponent() and CleanupEmptyLists() as a finishing touch, to avoid inducing a spec violation with a duplicate bom-ref or publishing empty JSON lists Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 61 +++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 73127990..661acf3f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -399,14 +399,61 @@ bom.SerialNumber is null }); } + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + + return result; + } + + /// + /// Merge main "metadata/component" entry with its possible alter-ego + /// in the components list and evict extra copy from that list: per + /// spec v1_4 at least, the bom-ref must be unique across the document. + /// + /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupMetadataComponent(Bom result) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); + if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) + { + if (iDebugLevel >= 2) + Console.WriteLine($"MERGE-CLEANUP: Searching in list"); + foreach (Component component in result.Components) + { + if (iDebugLevel >= 2) + Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); + if (component is null) continue; // should not happen + if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) + { + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); + result.Metadata.Component.mergeWith(component); + result.Components.Remove(component); + return result; + } + } + } + + if (iDebugLevel >= 1) + Console.WriteLine($"MERGE-CLEANUP: NO HITS"); + return result; + } + + public static Bom CleanupEmptyLists(Bom result) + { // cleanup empty top level elements - if (result.Metadata.Tools.Count == 0) result.Metadata.Tools = null; - if (result.Components.Count == 0) result.Components = null; - if (result.Services.Count == 0) result.Services = null; - if (result.ExternalReferences.Count == 0) result.ExternalReferences = null; - if (result.Dependencies.Count == 0) result.Dependencies = null; - if (result.Compositions.Count == 0) result.Compositions = null; - if (result.Vulnerabilities.Count == 0) result.Vulnerabilities = null; + if (result.Metadata?.Tools?.Count == 0) result.Metadata.Tools = null; + if (result.Components?.Count == 0) result.Components = null; + if (result.Services?.Count == 0) result.Services = null; + if (result.ExternalReferences?.Count == 0) result.ExternalReferences = null; + if (result.Dependencies?.Count == 0) result.Dependencies = null; + if (result.Compositions?.Count == 0) result.Compositions = null; + if (result.Vulnerabilities?.Count == 0) result.Vulnerabilities = null; return result; } From ce0afc6b3f00f290ca8581cd917c843e2c953f9a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 15:52:42 +0200 Subject: [PATCH 101/285] Merge.cs: be more careful about populating metadata/component vs. components[] array; use CleanupMetadataComponent() and CleanupEmptyLists() as a finishing touch, to avoid inducing a spec violation with a duplicate bom-ref or publishing empty JSON lists Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 53 ++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 661acf3f..bde7f240 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -165,6 +165,9 @@ public static partial class CycloneDXUtils /// public static Bom FlatMerge(Bom bom1, Bom bom2) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + var result = new Bom(); result.Metadata = new Metadata { @@ -186,10 +189,37 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var componentsMerger = new ListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); - //Add main component if missing - if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) + // Add main component from bom2 as a "yet another component" + // if missing in that list so far. Note: any more complicated + // cases should be handled by CleanupMetadataComponent() when + // called by MergeCommand or similar consumer; however we can + // not generally rely in a library that only one particular + // tool calls it - so this method should ensure validity of + // its own output on every step along the way. + if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) { - result.Components.Add(bom2.Metadata.Component); + // Skip such addition if the component in bom2 is same as the + // existing metadata/component in bom1 (gluing same file together + // twice should be effectively no-op); try to merge instead: + + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); + + if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) + || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) + { + // bom1's entry is not null and seems equivalent to bom2's: + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); + result.Metadata.Component = bom1.Metadata.Component; + result.Metadata.Component.mergeWith(bom2.Metadata.Component); + } + else + { + if (iDebugLevel >= 1) + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); + result.Components.Add(bom2.Metadata.Component); + } } var servicesMerger = new ListMergeHelper(); @@ -207,6 +237,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var vulnerabilitiesMerger = new ListMergeHelper(); result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities); + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + return result; } @@ -257,9 +290,14 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) if (bomSubject != null) { - // use the params provided if possible - result.Metadata.Component = bomSubject; - result.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + // use the params provided if possible: prepare a new document + // with desired "metadata/component" and merge differing data + // from earlier collected result into this structure. + var resultSubj = new Bom(); + + resultSubj.Metadata.Component = bomSubject; + resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + result = FlatMerge(resultSubj, result); var mainDependency = new Dependency(); mainDependency.Ref = result.Metadata.Component.BomRef; @@ -279,6 +317,9 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result.Dependencies.Add(mainDependency); } + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + return result; } From e0c615b9915c946163de2016693a32e18e2754e9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 21:37:41 +0200 Subject: [PATCH 102/285] Components.cs: mergeWith(): revise exception catching for NonNullable types Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 7cb99833..819268aa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -243,8 +243,8 @@ public bool mergeWith(Component obj) { try { property.SetValue(tmp, property.GetValue(this, null)); - } catch (System.Exception) { - // no-op + } catch (System.InvalidOperationException) { + // no-op, skip factually null values of NonNullable types } } bool mergedOk = true; @@ -269,7 +269,7 @@ public bool mergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { // Unspecified => required per CycloneDX spec v1.4?.. tmpItem = ComponentScope.Null; @@ -280,7 +280,7 @@ public bool mergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { objItem = ComponentScope.Null; } From f1776093be8a113f1c2d57dd1eec7753de3ecce6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 4 Aug 2023 21:38:06 +0200 Subject: [PATCH 103/285] Components.cs: mergeWith(): fix equality check for list items Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 64 ++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 819268aa..8692999e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -375,6 +375,7 @@ public bool mergeWith(Component obj) var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); var methodMergeWith = TType.GetMethod("mergeWith"); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); for (int o = 0; o < propValObjCount; o++) { @@ -388,29 +389,56 @@ public bool mergeWith(Component obj) var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); if (tmpItem != null) { - listHit = true; - if (methodMergeWith != null) + // EQ CHECK + bool propsSeemEqual = false; + bool propsSeemEqualLearned = false; + + try { - try - { - if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); - if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) - mergedOk = false; - } - catch (System.Exception exc) + if (methodEquals != null) { - if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); - mergedOk = false; + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); + propsSeemEqualLearned = true; } - } // else: no method, just trust equality - avoid "Add" to merge below - else + } + catch (System.Exception exc) { - if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + // no-op + if (iDebugLevel >= 5) + Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } - } // else: tmpitem considered not equal, should be added + + if (propsSeemEqual || !propsSeemEqualLearned) + { + // Got an equivalently-looking item on both sides! + // If there is no mergeWith() in its class, consider + // the two entries just equal (no-op to merge them). + listHit = true; + if (methodMergeWith != null) + { + try + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) + mergedOk = false; + } + catch (System.Exception exc) + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + mergedOk = false; + } + } // else: no method, just trust equality - avoid "Add" to merge below + else + { + if (iDebugLevel >= 6) + Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + } + } // else: tmpitem considered not equal, should be added + } } if (!listHit) From 0b6d1ce1fe7037d6361bac66729934212014c8e1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 6 Aug 2023 02:10:11 +0200 Subject: [PATCH 104/285] Revert "Components.cs: mergeWith(): revise exception catching for NonNullable types" This reverts commit 9ead840bef0353b24ee26995e981ed39829b35e2. Seems to be involved in more un-deduped entries than without it. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 8692999e..c6b87a0a 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -243,8 +243,8 @@ public bool mergeWith(Component obj) { try { property.SetValue(tmp, property.GetValue(this, null)); - } catch (System.InvalidOperationException) { - // no-op, skip factually null values of NonNullable types + } catch (System.Exception) { + // no-op } } bool mergedOk = true; @@ -269,7 +269,7 @@ public bool mergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.InvalidOperationException) + catch (System.Exception) { // Unspecified => required per CycloneDX spec v1.4?.. tmpItem = ComponentScope.Null; @@ -280,7 +280,7 @@ public bool mergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.InvalidOperationException) + catch (System.Exception) { objItem = ComponentScope.Null; } From c7af4ce8ddbfbdd9a9d42c124a84f4d253b72a83 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 13:34:25 +0200 Subject: [PATCH 105/285] Introduce a CycloneDX.Core/Models/BomEntity.cs as a base class for shared features Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 6 + src/CycloneDX.Core/Models/BomEntity.cs | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/CycloneDX.Core/Models/BomEntity.cs diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index ecaf4380..2348fc9d 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -58,6 +58,12 @@ public static string Serialize(Bom bom) return jsonBom; } + internal static string Serialize(BomEntity entity) + { + Contract.Requires(entity != null); + return JsonSerializer.Serialize(entity, _options); + } + internal static string Serialize(Component component) { Contract.Requires(component != null); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs new file mode 100644 index 00000000..4a1c8fde --- /dev/null +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -0,0 +1,144 @@ +// This file is part of CycloneDX Library for .NET +// +// 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. +// +// 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.Linq; +using System.Text.Json.Serialization; + +namespace CycloneDX.Models +{ + [Serializable] + class BomEntityConflictException : Exception + { + public BomEntityConflictException() + : base(String.Format("Unresolvable conflict in Bom entities")) + { } + + public BomEntityConflictException(Type type) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type.ToString())) + { } + + public BomEntityConflictException(string msg) + : base(String.Format("Unresolvable conflict in Bom entities: {0}", msg)) + { } + + public BomEntityConflictException(string msg, Type type) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) + { } + } + [Serializable] + class BomEntityIncompatibleException : Exception + { + public BomEntityIncompatibleException() + : base(String.Format("Comparing incompatible Bom entities")) + { } + + public BomEntityIncompatibleException(Type type1, Type type2) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1.ToString(), type2.ToString())) + { } + + public BomEntityIncompatibleException(string msg) + : base(String.Format("Comparing incompatible Bom entities: {0}", msg)) + { } + + public BomEntityIncompatibleException(string msg, Type type1, Type type2) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1.ToString(), type2.ToString(), msg)) + { } + } + + /// + /// BomEntity is intended as a base class for other classes in CycloneDX.Models, + /// which in turn encapsulate different concepts and data types described by + /// the specification. It allows them to share certain behaviors such as the + /// ability to determine "equivalent but not equal" objects (e.g. two instances + /// of a Component with the same "bom-ref" but different in some properties), + /// and to define the logic for merge-ability of such objects while coding much + /// of the logical scaffolding only once. + /// + public class BomEntity : IEquatable + { + protected BomEntity() + { + // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); + } + + public bool Equals(BomEntity other) + { + if (other is null || this.GetType() != other.GetType()) return false; + return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(other); + } + + public override int GetHashCode() + { + return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + } + + /// + /// Do this and other objects describe the same real-life entity? + /// Override this in sub-classes that have a more detailed definition of + /// equivalence (e.g. that certain fields are equal even if whole contents + /// are not). + /// + /// Another object of same type + /// True if two data objects are considered to represent + /// the same real-life entity, False otherwise. + public bool Equivalent(BomEntity other) + { + return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); + } + + /// + /// Default implementation just "agrees" that Equals()==true objects + /// are already merged (returns true), and that Equivalent()==false + /// objects are not (returns false), and for others (equivalent but + /// not equal, or different types) raises an exception. + /// Treats a null "other" object as a success (it is effectively a + /// no-op merge, which keeps "this" object as is). + /// + /// Another object of same type whose additional + /// non-conflicting data we try to squash into this object. + /// True if merge was successful, False if it these objects + /// are not equivalent, or throws if merge can not be done (including + /// lack of merge logic or unresolvable conflicts in data points). + /// + /// Source data problem: two entities with conflicting information + /// Caller error: somehow merging different entity types + public bool MergeWith(BomEntity other) + { + if (other is null) return true; + if (this.GetType() != other.GetType()) + { + // Note: potentially descendent classes can catch this + // to adapt their behavior... if some two different + // classes would ever describe something comparable + // in real life. + throw new BomEntityIncompatibleException(this.GetType(), other.GetType()); + } + + if (this.Equals(other)) return true; + if (!this.Equivalent(other)) return false; + + // Normal mode of operation: descendant classes catch this + // exception to use their custom non-trivial merging logic. + throw new BomEntityConflictException( + "Base-method implementation treats equivalent but not equal entities as conflicting", + this.GetType()); + } + } +} From 3ef0d36e3b1db4f9d39bade6dde04de2a3911b75 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 13:37:14 +0200 Subject: [PATCH 106/285] Hash.cs: implement as a BomEntity descendant class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Hash.cs | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Hash.cs b/src/CycloneDX.Core/Models/Hash.cs index 567f8446..4f54fa6f 100644 --- a/src/CycloneDX.Core/Models/Hash.cs +++ b/src/CycloneDX.Core/Models/Hash.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [XmlType("hash")] [ProtoContract] - public class Hash + public class Hash : BomEntity { [ProtoContract] public enum HashAlgorithm @@ -62,5 +62,40 @@ public enum HashAlgorithm [XmlText] [ProtoMember(2)] public string Content { get; set; } + + public bool Equivalent(Hash other) + { + return (!(other is null) && this.Alg == other.Alg); + } + + public bool MergeWith(Hash other) + { + try + { + // Basic checks for null, type compatibility, + // equality and non-equivalence; throws for + // the hard stuff to implement in the catch: + return base.MergeWith(other); + } + catch (BomEntityConflictException) + { + // Note: Alg is non-nullable so no check for that + if (this.Content is null && !(other.Content is null)) + { + this.Content = other.Content; + return true; + } + + if (this.Content != other.Content) + { + throw new BomEntityConflictException("Two Hash objects with same Alg='${this.Alg}' and different Content: '${this.Content}' vs. '${other.Content}'"); + } + + // All known properties merged or were equal/equivalent + return true; + } + + // Should not get here + } } } \ No newline at end of file From 9b4ea8bce558597e3239e71da4a45a44caf98f02 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:15:08 +0200 Subject: [PATCH 107/285] Restore simplistic CycloneDX.Utils/Merge.cs logic for ListMergeHelper: move the BomEntity related complexity to CycloneDX.Core/BomUtils.cs (initially "as is") Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 122 +++++++++++++++++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 120 +++----------------------------- 2 files changed, 133 insertions(+), 109 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 4e522578..50f97ae2 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -238,5 +238,127 @@ public static void EnumerateAllServices(Bom bom, Action callback) } } } + + public static List MergeBomEntityLists(List list1, List list2) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); + if (list1 is null) return list2; + if (list2 is null) return list1; + + List result = new List(list1); + + var TType = ((T)list2[0]).GetType(); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + + foreach (var item2 in list2) + { + bool isContained = false; + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + for (int i=0; i < result.Count; i++) + { + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + T item1 = result[i]; + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there: + if (methodMergeWith != null) + { + try + { + if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + } + catch (System.Exception exc) + { + if (iDebugLevel >= 1) + Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } // else: That class lacks a mergeWith(), gotta trust the equality + else + { + if (iDebugLevel >= 6) + Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); + if (item1 is IEquatable) + { + if (methodEquals != null) + { + try + { + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: try methodEquals()"); + if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) + { + isContained = true; + break; + } + } + catch (System.Exception exc) + { + if (iDebugLevel >= 5) + Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); + } + } + + if (item1.Equals(item2)) + { + // Fall back to generic equality check which may be useless + if (iDebugLevel >= 3) + Console.WriteLine($"SKIP MERGE: items say they are equal"); + isContained = true; + break; // items deemed equivalent + } + + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items say they are not equal"); + } + else + { + if (iDebugLevel >= 3) + Console.WriteLine($"MERGE: items are not IEquatable"); + } +/* + else + { + if (item1 is CycloneDX.Models.Bom) + { + if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) + { + isContained = true; + break; // items deemed equivalent + } + } + } +*/ + } + } + + if (!isContained) + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + else + { + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } + } + + return result; + } } } \ No newline at end of file diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index bde7f240..ce6fc20e 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,120 +27,22 @@ class ListMergeHelper { public List Merge(List list1, List list2) { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); - if (list1 is null) return list2; - if (list2 is null) return list1; - - List result = new List(list1); + if (((T)list2[0]).GetType() is BomEntity) + { + return BomUtils.MergeBomEntityLists(list1, list2); + } - var TType = ((T)list2[0]).GetType(); - var methodMergeWith = TType.GetMethod("mergeWith"); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + // Lists of legacy types + var result = new List(list1); - foreach (var item2 in list2) + foreach (var item in list2) { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); - for (int i=0; i < result.Count; i++) + if (!(result.Contains(item))) { - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - T item1 = result[i]; - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there: - if (methodMergeWith != null) - { - try - { - if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 1) - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } // else: That class lacks a mergeWith(), gotta trust the equality - else - { - if (iDebugLevel >= 6) - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1 is IEquatable) - { - if (methodEquals != null) - { - try - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: try methodEquals()"); - if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } - - if (item1.Equals(item2)) - { - // Fall back to generic equality check which may be useless - if (iDebugLevel >= 3) - Console.WriteLine($"SKIP MERGE: items say they are equal"); - isContained = true; - break; // items deemed equivalent - } - - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items say they are not equal"); - } - else - { - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items are not IEquatable"); - } -/* - else - { - if (item1 is CycloneDX.Models.Bom) - { - if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) - { - isContained = true; - break; // items deemed equivalent - } - } - } -*/ - } - } - - if (!isContained) - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } - else - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + result.Add(item); } } From 42d96c395ad2c4989c2eefa2e3a4786275518d29 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:41:37 +0200 Subject: [PATCH 108/285] Restore simplistic CycloneDX.Utils/Merge.cs logic for ListMergeHelper: move the BomEntity related complexity to CycloneDX.Core/BomUtils.cs (refactor for BomEntity) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 136 ++++++++++++--------------------- src/CycloneDX.Utils/Merge.cs | 15 +++- 2 files changed, 62 insertions(+), 89 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 50f97ae2..51f2a5a4 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -244,121 +244,83 @@ public static List MergeBomEntityLists(List list1, List= 1) - Console.WriteLine($"List-Merge for: {this.GetType().ToString()}"); - if (list1 is null) return list2; - if (list2 is null) return list1; + Console.WriteLine($"List-Merge for: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - List result = new List(list1); + // Check actual subtypes of list entries + // TODO: Reflection to get generic List<> type argument? + // This would avoid lists of mixed BomEntity descendant objects + // typed truly as a List by caller... + Type TType = list1[0].GetType(); + Type TType2 = list2[0].GetType(); + if (TType == typeof(BomEntity) || TType2 == typeof(BomEntity)) + { + // Should not happen, but... + throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types (one of these seems to be the base class)", TType, TType2); + } + if (TType != TType2) + { + throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types", TType, TType2); + } - var TType = ((T)list2[0]).GetType(); - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listType = typeof(List<>); + var constructedListType = listType.MakeGenericType(TType); + List result = (List)Activator.CreateInstance(constructedListType); + result.AddRange(list1); foreach (var item2 in list2) { bool isContained = false; if (iDebugLevel >= 3) Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + for (int i=0; i < result.Count; i++) { if (iDebugLevel >= 3) Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - T item1 = result[i]; + var item1 = result[i]; + // Squash contents of the new entry with an already // existing equivalent (same-ness is subject to // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there: - if (methodMergeWith != null) + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + if (item1.MergeWith(item2)) { - try - { - if (((bool)methodMergeWith.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 1) - Console.WriteLine($"SKIP MERGE: can not mergeWith() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } // else: That class lacks a mergeWith(), gotta trust the equality - else - { - if (iDebugLevel >= 6) - Console.WriteLine($"SKIP MERGE? can not mergeWith() {item1.ToString()} and {item2.ToString()}: no such method"); - if (item1 is IEquatable) - { - if (methodEquals != null) - { - try - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: try methodEquals()"); - if (((bool)methodEquals.Invoke(item1, new object[] {item2}))) - { - isContained = true; - break; - } - } - catch (System.Exception exc) - { - if (iDebugLevel >= 5) - Console.WriteLine($"LIST-MERGE: can not check Equals() {item1.ToString()} and {item2.ToString()}: {exc.ToString()}"); - } - } - - if (item1.Equals(item2)) - { - // Fall back to generic equality check which may be useless - if (iDebugLevel >= 3) - Console.WriteLine($"SKIP MERGE: items say they are equal"); - isContained = true; - break; // items deemed equivalent - } - - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items say they are not equal"); - } - else - { - if (iDebugLevel >= 3) - Console.WriteLine($"MERGE: items are not IEquatable"); - } -/* - else - { - if (item1 is CycloneDX.Models.Bom) - { - if (CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item1) == CycloneDX.Json.Serializer.Serialize((CycloneDX.Models.Bom)item2)) - { - isContained = true; - break; // items deemed equivalent - } - } - } -*/ + isContained = true; + break; // item2 merged into result[item1] or already equal to it } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. } - if (!isContained) + if (isContained) { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } else { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); } } return result; - } + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index ce6fc20e..3c4548e1 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -30,9 +30,9 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (((T)list2[0]).GetType() is BomEntity) + if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - return BomUtils.MergeBomEntityLists(list1, list2); + return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } // Lists of legacy types @@ -52,6 +52,17 @@ public List Merge(List list1, List list2) public static partial class CycloneDXUtils { + // TOTHINK: Now that we have a BomEntity base class, shouldn't + // this logic relocate to become a Bom.MergeWith() implementation? + // Notably, sanity checks like CleanupMetadataComponent and making + // sure that a Bom+Bom merge produces a spec-validatable result + // should be a concern of that class (same as we coerce other + // classes to perform a structure-dependent meaningful merge, + // and same as the types in its source code handle non-nullable + // properties, etc.) - right?.. Perhaps sub-classes like BomFlat + // and BomHierarchical and their respective MergeWith() methods + // could be a way forward for this... + /// /// Performs a flat merge of two BOMs. /// From 8920767217c1c70be9ce691ad018f4cf26dbec90 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 14:56:06 +0200 Subject: [PATCH 109/285] Component.cs: implement as a BomEntity descendant class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 69 ++++++++++++++------------ src/CycloneDX.Utils/Merge.cs | 4 +- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c6b87a0a..0150e7fa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -29,7 +29,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("component")] [ProtoContract] - public class Component: IEquatable + public class Component: BomEntity { [ProtoContract] public enum Classification @@ -209,7 +209,12 @@ public override int GetHashCode() return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } - public bool mergeWith(Component obj) + public bool Equivalent(Component obj) + { + return (!(obj is null) && this.BomRef == obj.BomRef); + } + + public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -217,7 +222,7 @@ public bool mergeWith(Component obj) if (this.Equals(obj)) { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): SKIP: contents are identical, nothing to do"); + Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); return true; } @@ -228,10 +233,10 @@ public bool mergeWith(Component obj) // Objects seem equivalent according to critical arguments; // merge the attribute values with help of reflection: if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases @@ -252,7 +257,7 @@ public bool mergeWith(Component obj) foreach (PropertyInfo property in properties) { if (iDebugLevel >= 2) - Console.WriteLine($"Component.mergeWith(): <{property.PropertyType}>'{property.Name}'"); + Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -286,7 +291,7 @@ public bool mergeWith(Component obj) } if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) @@ -294,7 +299,7 @@ public bool mergeWith(Component obj) // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Required'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } @@ -305,7 +310,7 @@ public bool mergeWith(Component obj) // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): SCOPE: set 'Excluded'"); + Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -315,7 +320,7 @@ public bool mergeWith(Component obj) // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); + Console.WriteLine($"Component.MergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); mergedOk = false; } break; @@ -327,7 +332,7 @@ public bool mergeWith(Component obj) bool objItem = (bool)property.GetValue(obj, null); if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); } @@ -341,7 +346,7 @@ public bool mergeWith(Component obj) if (propValTmp == null && propValObj == null) { if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): LIST?: got in tmp and in obj"); + Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -352,7 +357,7 @@ public bool mergeWith(Component obj) if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + Console.WriteLine($"Component.MergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); mergedOk = false; continue; } @@ -360,7 +365,7 @@ public bool mergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount < 1) { @@ -374,7 +379,7 @@ public bool mergeWith(Component obj) } var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); - var methodMergeWith = TType.GetMethod("mergeWith"); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); for (int o = 0; o < propValObjCount; o++) @@ -398,7 +403,7 @@ public bool mergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); propsSeemEqualLearned = true; } @@ -407,7 +412,7 @@ public bool mergeWith(Component obj) { // no-op if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } if (propsSeemEqual || !propsSeemEqualLearned) @@ -421,21 +426,21 @@ public bool mergeWith(Component obj) try { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); + Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added } @@ -461,12 +466,12 @@ public bool mergeWith(Component obj) // followed by 'Scope' // which we specially handle above if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP NullableAttribute"); + Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); continue; } if (iDebugLevel >= 3) - Console.WriteLine($"Component.mergeWith(): DEFAULT TYPES"); + Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -490,7 +495,7 @@ public bool mergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): try methodEquals()"); + Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; } @@ -499,7 +504,7 @@ public bool mergeWith(Component obj) { // no-op if (iDebugLevel >= 5) - Console.WriteLine($"Component.mergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } try @@ -508,7 +513,7 @@ public bool mergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): MIGHT SKIP MERGE: items say they are equal"); + Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -524,7 +529,7 @@ public bool mergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: items say they are equal"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -537,10 +542,10 @@ public bool mergeWith(Component obj) if (!propsSeemEqual) { if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): items say they are not equal"); + Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } - var methodMergeWith = TType.GetMethod("mergeWith"); + var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); if (methodMergeWith != null) { try @@ -554,7 +559,7 @@ public bool mergeWith(Component obj) if (propsSeemEqual) continue; if (iDebugLevel >= 4) - Console.WriteLine($"Component.mergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } } @@ -564,7 +569,7 @@ public bool mergeWith(Component obj) if (propsSeemEqual) continue; if (iDebugLevel >= 6) - Console.WriteLine($"Component.mergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } } @@ -581,13 +586,13 @@ public bool mergeWith(Component obj) } if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + Console.WriteLine($"Component.MergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); return mergedOk; } else { if (iDebugLevel >= 1) - Console.WriteLine($"Component.mergeWith(): SKIP: items do not seem related"); + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3c4548e1..9eb1a8bc 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -125,7 +125,7 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) if (iDebugLevel >= 1) Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); result.Metadata.Component = bom1.Metadata.Component; - result.Metadata.Component.mergeWith(bom2.Metadata.Component); + result.Metadata.Component.MergeWith(bom2.Metadata.Component); } else { @@ -386,7 +386,7 @@ public static Bom CleanupMetadataComponent(Bom result) { if (iDebugLevel >= 1) Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); - result.Metadata.Component.mergeWith(component); + result.Metadata.Component.MergeWith(component); result.Components.Remove(component); return result; } From 99b66444240b0a4d1634ba8eff6eafe92793c7ea Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 15:05:40 +0200 Subject: [PATCH 110/285] CycloneDX.Utils/Merge.cs: debug trace if proceeding with legacy logic Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 9eb1a8bc..7ba766bd 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -27,6 +27,9 @@ class ListMergeHelper { public List Merge(List list1, List list2) { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; @@ -35,7 +38,9 @@ public List Merge(List list1, List list2) return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } - // Lists of legacy types + // Lists of legacy types + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); var result = new List(list1); foreach (var item in list2) From 4901a93295ea099cc6cb9f2e8689df2ac69a2fe0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:40:56 +0200 Subject: [PATCH 111/285] BomUtils.cs: MergeBomEntityLists(): refactor so it builds Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 51f2a5a4..84698909 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -17,6 +17,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Collections; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -266,12 +268,21 @@ public static List MergeBomEntityLists(List list1, List "result" at run-time: Type listType = typeof(List<>); var constructedListType = listType.MakeGenericType(TType); - List result = (List)Activator.CreateInstance(constructedListType); - result.AddRange(list1); + //IList result = (IList)Activator.CreateInstance(constructedListType); //.Cast().ToList(); + var result = Activator.CreateInstance(constructedListType); + + foreach (var item1 in list1) + { + result.Add(item1); + } +*/ + + List result = new List(list1); foreach (var item2 in list2) { @@ -320,7 +331,7 @@ public static List MergeBomEntityLists(List list1, List)result; } } } From ada2fe501977b958e8b1dcb090d68cab086a6fea Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:43:09 +0200 Subject: [PATCH 112/285] BomEntity: make exception classes public Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 4a1c8fde..5d60dad4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [Serializable] - class BomEntityConflictException : Exception + public class BomEntityConflictException : Exception { public BomEntityConflictException() : base(String.Format("Unresolvable conflict in Bom entities")) @@ -42,8 +42,9 @@ public BomEntityConflictException(string msg, Type type) : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) { } } + [Serializable] - class BomEntityIncompatibleException : Exception + public class BomEntityIncompatibleException : Exception { public BomEntityIncompatibleException() : base(String.Format("Comparing incompatible Bom entities")) From 75fff60847ce16ff571b3128ce7c35432066d2e4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:45:16 +0200 Subject: [PATCH 113/285] Merge.cs: update comment Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 7ba766bd..21c9daeb 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -38,7 +38,7 @@ public List Merge(List list1, List list2) return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; } - // Lists of legacy types + // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); var result = new List(list1); From ce2998548698ec8ab1717403bbd35d53223104fc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:46:03 +0200 Subject: [PATCH 114/285] Tool.cs: subclass from BomEntity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Tool.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index f6a6d145..60353ef7 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Tool: IEquatable + public class Tool: BomEntity { [XmlElement("vendor")] [ProtoMember(1)] @@ -49,12 +49,14 @@ public class Tool: IEquatable public bool Equals(Tool obj) { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); + /*return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj);*/ + return base.Equals(obj); } public override int GetHashCode() { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + /*return CycloneDX.Json.Serializer.Serialize(this).GetHashCode();*/ + return base.GetHashCode(); } } -} \ No newline at end of file +} From 497c0a1af3084a27a14a8a48534383ba1738ed54 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 20:47:30 +0200 Subject: [PATCH 115/285] Move BomUtils:MergeBomEntityLists() to BomEntityListMergeHelper class for consistency Signed-off-by: Jim Klimov --- src/CycloneDX.Core/BomUtils.cs | 95 -------------------------- src/CycloneDX.Core/Models/BomEntity.cs | 68 ++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 6 +- 3 files changed, 72 insertions(+), 97 deletions(-) diff --git a/src/CycloneDX.Core/BomUtils.cs b/src/CycloneDX.Core/BomUtils.cs index 84698909..16177edb 100644 --- a/src/CycloneDX.Core/BomUtils.cs +++ b/src/CycloneDX.Core/BomUtils.cs @@ -17,8 +17,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Collections; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -240,98 +238,5 @@ public static void EnumerateAllServices(Bom bom, Action callback) } } } - - public static List MergeBomEntityLists(List list1, List list2) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; - - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; - - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - - // Check actual subtypes of list entries - // TODO: Reflection to get generic List<> type argument? - // This would avoid lists of mixed BomEntity descendant objects - // typed truly as a List by caller... - Type TType = list1[0].GetType(); - Type TType2 = list2[0].GetType(); - if (TType == typeof(BomEntity) || TType2 == typeof(BomEntity)) - { - // Should not happen, but... - throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types (one of these seems to be the base class)", TType, TType2); - } - if (TType != TType2) - { - throw new BomEntityIncompatibleException("Can not merge lists of different Bom entity types", TType, TType2); - } - -/* - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listType = typeof(List<>); - var constructedListType = listType.MakeGenericType(TType); - //IList result = (IList)Activator.CreateInstance(constructedListType); //.Cast().ToList(); - var result = Activator.CreateInstance(constructedListType); - - foreach (var item1 in list1) - { - result.Add(item1); - } -*/ - - List result = new List(list1); - - foreach (var item2 in list2) - { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); - - for (int i=0; i < result.Count; i++) - { - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - var item1 = result[i]; - - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there. - // For BomEntity descendant instances we assume that - // they have Equals(), Equivalent() and MergeWith() - // methods defined or inherited as is suitable for - // the particular entity type, hence much less code - // and error-checking than there was in the PoC: - if (item1.MergeWith(item2)) - { - isContained = true; - break; // item2 merged into result[item1] or already equal to it - } - // MergeWith() may throw BomEntityConflictException which we - // want to propagate to users - their input data is confusing. - // Probably should not throw BomEntityIncompatibleException - // unless the lists truly are of mixed types. - } - - if (isContained) - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); - } - else - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } - } - - return (List)result; - } } } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 5d60dad4..26fb82b7 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -63,6 +63,74 @@ public BomEntityIncompatibleException(string msg, Type type1, Type type2) { } } + public class BomEntityListMergeHelper where T : BomEntity + { + public List Merge(List list1, List list2) + { + //return BomUtils.MergeBomEntityLists(list1, list2); + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for BomEntity derivatives: {list1.GetType().ToString()}"); + + List result = new List(list1); + Type TType = list1[0].GetType(); + + foreach (var item2 in list2) + { + bool isContained = false; + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + + for (int i=0; i < result.Count; i++) + { + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + var item1 = result[i]; + + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + if (item1.MergeWith(item2)) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. + } + + if (isContained) + { + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } + else + { + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); + } + } + + return result; + } + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 21c9daeb..e78e7260 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -97,14 +97,16 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) Timestamp = DateTime.Now }; - var toolsMerger = new ListMergeHelper(); + var toolsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); + //var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); + //var tools = BomUtils.MergeBomEntityLists(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { result.Metadata.Tools = tools; } - var componentsMerger = new ListMergeHelper(); + var componentsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); // Add main component from bom2 as a "yet another component" From a6b1ee225cdaa3aecd04f95d8f2cd299db400447 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 7 Aug 2023 22:47:33 +0200 Subject: [PATCH 116/285] Merge.cs: unite back uses of BomEntityListMergeHelper and legacy ListMergeHelper so consumers/callers do not have to change Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index e78e7260..d637f394 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -35,7 +35,23 @@ public List Merge(List list1, List list2) if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - return BomUtils.MergeBomEntityLists(list1 as List, list2 as List) as List; + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } } // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) @@ -97,16 +113,14 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) Timestamp = DateTime.Now }; - var toolsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); - //var toolsMerger = new ListMergeHelper(); + var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); - //var tools = BomUtils.MergeBomEntityLists(bom1.Metadata?.Tools, bom2.Metadata?.Tools); if (tools != null) { result.Metadata.Tools = tools; } - var componentsMerger = new CycloneDX.Models.BomEntityListMergeHelper(); + var componentsMerger = new ListMergeHelper(); result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); // Add main component from bom2 as a "yet another component" From b934207a2d002e4f46784095d9f07b68cc8c1da3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 08:59:14 +0200 Subject: [PATCH 117/285] BomEntity.cs: refactor with SerializeEntity() helper Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 26fb82b7..789cdc2d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -147,15 +147,25 @@ protected BomEntity() // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); } + /// + /// Helper for comparisons and getting object hash code. + /// Calls our standard CycloneDX.Json.Serializer to use + /// its common options in particular. + /// + internal string SerializeEntity() + { + return CycloneDX.Json.Serializer.Serialize(this); + } + public bool Equals(BomEntity other) { if (other is null || this.GetType() != other.GetType()) return false; - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(other); + return this.SerializeEntity() == other.SerializeEntity(); } public override int GetHashCode() { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + return this.SerializeEntity().GetHashCode(); } /// From efcd3ba533d3dbf0aae1fc1ffe982c75d0248601 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 11:41:02 +0200 Subject: [PATCH 118/285] BomEntity.cs: fix SerializeEntity() to find custom serializer method if defined for the type Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 789cdc2d..569e1011 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -154,7 +154,21 @@ protected BomEntity() /// internal string SerializeEntity() { - return CycloneDX.Json.Serializer.Serialize(this); + // Do we have a custom serializer defined? Use it! + // (One for BomEntity tends to serialize this base class + // so comes up empty, or has to jump through hoops...) + var myClassType = typeof(CycloneDX.Json.Serializer); + var methodSerializeThis = myClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { this.GetType() }); + if (methodSerializeThis != null) + { + var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); + return res1; + } + + var res = CycloneDX.Json.Serializer.Serialize(this); + return res; } public bool Equals(BomEntity other) From 73fbb8e52d195aad795af1d7db5d723613b3dd1b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 14:26:25 +0200 Subject: [PATCH 119/285] BomEntity.cs: pre-cache info about custom serializer method if defined for the type (avoid looping main logic with many reflection queries) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 52 +++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 569e1011..6f91b78b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; namespace CycloneDX.Models @@ -142,6 +143,51 @@ public List Merge(List list1, List list2) /// public class BomEntity : IEquatable { + // Keep this info initialized once to cut down on overheads of reflection + // when running in our run-time loops. + // Thanks to https://stackoverflow.com/a/45896403/4715872 for the Func'y trick + // and https://stackoverflow.com/questions/857705/get-all-derived-types-of-a-type + // TOTHINK: Should these be exposed as public or hidden even more strictly? + // Perhaps add getters for a copy? + + /// + /// List of classes derived from BomEntity, prepared startically at start time. + /// + static List KnownEntityTypes = + new Func>(() => + { + List derived_types = new List(); + foreach (var domain_assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var assembly_types = domain_assembly.GetTypes() + .Where(type => type.IsSubclassOf(typeof(BomEntity)) && !type.IsAbstract); + + derived_types.AddRange(assembly_types); + } + return derived_types; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() + /// implementations (if present), prepared startically at start time. + /// + static Dictionary KnownTypeSerializers = + new Func>(() => + { + var jserClassType = typeof(CycloneDX.Json.Serializer); + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = jserClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + protected BomEntity() { // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); @@ -157,11 +203,7 @@ internal string SerializeEntity() // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class // so comes up empty, or has to jump through hoops...) - var myClassType = typeof(CycloneDX.Json.Serializer); - var methodSerializeThis = myClassType.GetMethod("Serialize", - BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, - new Type[] { this.GetType() }); - if (methodSerializeThis != null) + if (KnownTypeSerializers.TryGetValue(this.GetType(), out var methodSerializeThis)) { var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); return res1; From eb67d8be5391e340ab8294786e2232ccebc3dd49 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 14:27:29 +0200 Subject: [PATCH 120/285] BomEntity.cs: pre-cache info about Equals(), Equivalent() and MergeWith() overrides in derived BomEntity classes (avoid looking for this info in a loop) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 72 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 6f91b78b..0e5cdaf7 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -188,6 +188,66 @@ public class BomEntity : IEquatable return dict; }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equals() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equivalent() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeEquivalent = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom MergeWith() method implementations + /// (if present), prepared startically at start time. + /// + static Dictionary KnownTypeMergeWith = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("MergeWith", + BindingFlags.Public | BindingFlags.NonPublic, + new Type[] { type }); + if (method != null) + dict[type] = method; + } + return dict; + }) (); + protected BomEntity() { // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); @@ -267,7 +327,17 @@ public bool MergeWith(BomEntity other) } if (this.Equals(other)) return true; - if (!this.Equivalent(other)) return false; + // Avoid calling Equals => serializer twice for no gain + // (default equivalence is equality): + if (KnownTypeEquivalent.TryGetValue(this.GetType(), out var methodEquivalent)) + { + if (!this.Equivalent(other)) return false; + // else fall through to exception below + } + else + { + return false; // known not equal => not equivalent by default => false + } // Normal mode of operation: descendant classes catch this // exception to use their custom non-trivial merging logic. From 9b78d6a38a0c6cc931bae578b627849f71c36d3e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 16:54:50 +0200 Subject: [PATCH 121/285] Merge.cs: ListMergeHelper: use idiomatic and more efficient typeof(T) rather than list1[0].GetType() Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index d637f394..71fc47b8 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -38,7 +38,7 @@ public List Merge(List list1, List list2) // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); var helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); From ae5a84b58f291b59c3706222e2cb98b9105c2e97 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:14:01 +0200 Subject: [PATCH 122/285] BomEntity: forward from default Equals() and Equivalent() implems to class-customized ones if present Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0e5cdaf7..eaa59c89 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -273,8 +273,23 @@ internal string SerializeEntity() return res; } + /// + /// NOTE: Class methods do not "override" this one because they compare to their type + /// and not to the base BomEntity type objects. They should also not call this method + /// to avoid looping - implement everything needed there directly, if ever needed! + /// Keep in mind that the base implementation calls the SerializeEntity() method which + /// should be by default aware and capable of ultimately serializing the properties + /// relevant to each derived class. + /// + /// Another BomEntity-derived object of same type + /// True if two objects are deemed equal public bool Equals(BomEntity other) { + if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquals)) + { + return (bool)methodEquals.Invoke(this, new object[] {other}); + } + if (other is null || this.GetType() != other.GetType()) return false; return this.SerializeEntity() == other.SerializeEntity(); } @@ -286,15 +301,21 @@ public override int GetHashCode() /// /// Do this and other objects describe the same real-life entity? - /// Override this in sub-classes that have a more detailed definition of + /// "Override" this in sub-classes that have a more detailed definition of /// equivalence (e.g. that certain fields are equal even if whole contents - /// are not). + /// are not) by defining an implementation tailored to that derived type + /// as the argument, or keep this default where equiality is equivalence. /// /// Another object of same type /// True if two data objects are considered to represent /// the same real-life entity, False otherwise. public bool Equivalent(BomEntity other) { + if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquivalent)) + { + return (bool)methodEquivalent.Invoke(this, new object[] {other}); + } + return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); } From ce746c1a3312e1ce7809dc71d03aa8e22aa1c44b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:14:18 +0200 Subject: [PATCH 123/285] Move ListMergeHelper from Merge.cs to CycloneDX.Core so it can be shared by different codebase more efficiently Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 81 +++++++++++++++++++++++++++ src/CycloneDX.Utils/Merge.cs | 49 +--------------- 2 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 src/CycloneDX.Core/ListMergeHelper.cs diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs new file mode 100644 index 00000000..e9ab7adb --- /dev/null +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -0,0 +1,81 @@ +// This file is part of CycloneDX Library for .NET +// +// 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. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using CycloneDX.Models; + +namespace CycloneDX +{ + /// + /// Allows to merge generic lists with items of specified types + /// (by default essentially adding entries which are not present + /// yet according to List.Contains() method), and calls special + /// logic for lists of BomEntry types. + /// Used in CycloneDX.Utils various Merge implementations as well + /// as in CycloneDX.Core BomEntity-derived classes' MergeWith(). + /// + /// Type of listed entries + public class ListMergeHelper + { + public List Merge(List list1, List list2) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + iDebugLevel = 0; + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + + if (typeof(BomEntity).IsInstanceOfType(list1[0])) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } + } + + // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + var result = new List(list1); + + foreach (var item in list2) + { + if (!(result.Contains(item))) + { + result.Add(item); + } + } + + return result; + } + } +} diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 71fc47b8..184c9239 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,60 +17,13 @@ using System; using System.Collections.Generic; +using CycloneDX; using CycloneDX.Models; using CycloneDX.Models.Vulnerabilities; using CycloneDX.Utils.Exceptions; namespace CycloneDX.Utils { - class ListMergeHelper - { - public List Merge(List list1, List list2) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - iDebugLevel = 0; - - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; - - if (typeof(BomEntity).IsInstanceOfType(list1[0])) - { - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); - if (methodMerge != null) - { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); - } - else - { - // Should not get here, but if we do - log and fall through - if (iDebugLevel >= 1) - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - } - } - - // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) - if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - var result = new List(list1); - - foreach (var item in list2) - { - if (!(result.Contains(item))) - { - result.Add(item); - } - } - - return result; - } - } - public static partial class CycloneDXUtils { // TOTHINK: Now that we have a BomEntity base class, shouldn't From e2adbefa76c1c5f6f631c78f01d70aeb6b58daaa Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 17:38:42 +0200 Subject: [PATCH 124/285] CycloneDX.Core Model classes: make them all derivates of BomEntity so common equality, equivalence and merging methods are inherited and applied by default Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/AttachedText.cs | 2 +- src/CycloneDX.Core/Models/Bom.cs | 7 ++++++- src/CycloneDX.Core/Models/Commit.cs | 2 +- src/CycloneDX.Core/Models/Composition.cs | 2 +- src/CycloneDX.Core/Models/DataClassification.cs | 2 +- src/CycloneDX.Core/Models/Dependency.cs | 4 +++- src/CycloneDX.Core/Models/Diff.cs | 3 ++- src/CycloneDX.Core/Models/Evidence.cs | 2 +- src/CycloneDX.Core/Models/EvidenceCopyright.cs | 2 +- src/CycloneDX.Core/Models/ExternalReference.cs | 2 +- src/CycloneDX.Core/Models/IdentifiableAction.cs | 2 +- src/CycloneDX.Core/Models/Issue.cs | 2 +- src/CycloneDX.Core/Models/License.cs | 2 +- src/CycloneDX.Core/Models/LicenseChoice.cs | 2 +- src/CycloneDX.Core/Models/Metadata.cs | 2 +- src/CycloneDX.Core/Models/Note.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalContact.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalEntity.cs | 2 +- src/CycloneDX.Core/Models/Patch.cs | 2 +- src/CycloneDX.Core/Models/Pedigree.cs | 2 +- src/CycloneDX.Core/Models/Property.cs | 2 +- src/CycloneDX.Core/Models/ReleaseNotes.cs | 2 +- src/CycloneDX.Core/Models/Service.cs | 5 +++-- src/CycloneDX.Core/Models/Source.cs | 2 +- src/CycloneDX.Core/Models/Swid.cs | 2 +- src/CycloneDX.Core/Models/Tool.cs | 9 +++++---- src/CycloneDX.Core/Models/ValidationResult.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs | 2 +- .../Models/Vulnerabilities/AffectedVersions.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs | 2 +- .../Models/Vulnerabilities/Vulnerability.cs | 4 +++- 35 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/CycloneDX.Core/Models/AttachedText.cs b/src/CycloneDX.Core/Models/AttachedText.cs index 65f71fd0..b3626029 100644 --- a/src/CycloneDX.Core/Models/AttachedText.cs +++ b/src/CycloneDX.Core/Models/AttachedText.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class AttachedText + public class AttachedText : BomEntity { [XmlAttribute("content-type")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 53ddd363..393c0973 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -29,7 +29,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlRoot("bom", IsNullable=false)] [ProtoContract] - public class Bom + public class Bom : BomEntity { [XmlIgnore] public string BomFormat => "CycloneDX"; @@ -133,5 +133,10 @@ public int NonNullableVersion [ProtoMember(10)] public List Vulnerabilities { get; set; } public bool ShouldSerializeVulnerabilities() { return Vulnerabilities?.Count > 0; } + + // TODO: MergeWith() might be reasonable but is currently handled + // by several strategy implementations in CycloneDX.Utils Merge.cs + // so maybe there should be sub-classes or strategy arguments or + // properties to select one of those implementations at run-time?.. } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/Commit.cs b/src/CycloneDX.Core/Models/Commit.cs index bd0ea2d1..60efa995 100644 --- a/src/CycloneDX.Core/Models/Commit.cs +++ b/src/CycloneDX.Core/Models/Commit.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Commit + public class Commit : BomEntity { [XmlElement("uid")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index be2a55a0..c6643738 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 : BomEntity, IXmlSerializable { [ProtoContract] public enum AggregateType diff --git a/src/CycloneDX.Core/Models/DataClassification.cs b/src/CycloneDX.Core/Models/DataClassification.cs index 2cc2815a..a3df70a2 100644 --- a/src/CycloneDX.Core/Models/DataClassification.cs +++ b/src/CycloneDX.Core/Models/DataClassification.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DataClassification + public class DataClassification : BomEntity { [XmlAttribute("flow")] [ProtoMember(1, IsRequired=true)] diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index f2bd35c0..34b7dfb8 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("dependency")] [ProtoContract] - public class Dependency: IEquatable + public class Dependency : BomEntity { [XmlAttribute("ref")] [ProtoMember(1)] @@ -36,6 +36,7 @@ public class Dependency: IEquatable [ProtoMember(2)] public List Dependencies { get; set; } +/* public bool Equals(Dependency obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -45,5 +46,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/Diff.cs b/src/CycloneDX.Core/Models/Diff.cs index 3eda6e66..b2b5ddf4 100644 --- a/src/CycloneDX.Core/Models/Diff.cs +++ b/src/CycloneDX.Core/Models/Diff.cs @@ -21,11 +21,12 @@ namespace CycloneDX.Models { [ProtoContract] - public class Diff + public class Diff : BomEntity { [XmlElement("text")] [ProtoMember(1)] public AttachedText Text { get; set; } + [XmlElement("url")] [ProtoMember(2)] public string Url { get; set; } diff --git a/src/CycloneDX.Core/Models/Evidence.cs b/src/CycloneDX.Core/Models/Evidence.cs index dac69adc..c4ae5f38 100644 --- a/src/CycloneDX.Core/Models/Evidence.cs +++ b/src/CycloneDX.Core/Models/Evidence.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence")] [ProtoContract] - public class Evidence + public class Evidence : BomEntity { [XmlElement("licenses")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EvidenceCopyright.cs b/src/CycloneDX.Core/Models/EvidenceCopyright.cs index 03161b88..d045c22b 100644 --- a/src/CycloneDX.Core/Models/EvidenceCopyright.cs +++ b/src/CycloneDX.Core/Models/EvidenceCopyright.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class EvidenceCopyright + public class EvidenceCopyright : BomEntity { [XmlText] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index 97b2b709..84d12e5d 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores")] [ProtoContract] - public class ExternalReference + public class ExternalReference : BomEntity { [ProtoContract] public enum ExternalReferenceType diff --git a/src/CycloneDX.Core/Models/IdentifiableAction.cs b/src/CycloneDX.Core/Models/IdentifiableAction.cs index 8cc58365..205e65b2 100644 --- a/src/CycloneDX.Core/Models/IdentifiableAction.cs +++ b/src/CycloneDX.Core/Models/IdentifiableAction.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class IdentifiableAction + public class IdentifiableAction : BomEntity { private DateTime? _timestamp; [XmlElement("timestamp")] diff --git a/src/CycloneDX.Core/Models/Issue.cs b/src/CycloneDX.Core/Models/Issue.cs index d8f3f584..591a6eb8 100644 --- a/src/CycloneDX.Core/Models/Issue.cs +++ b/src/CycloneDX.Core/Models/Issue.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Issue + public class Issue : BomEntity { [ProtoContract] public enum IssueClassification diff --git a/src/CycloneDX.Core/Models/License.cs b/src/CycloneDX.Core/Models/License.cs index ee80a378..44438d62 100644 --- a/src/CycloneDX.Core/Models/License.cs +++ b/src/CycloneDX.Core/Models/License.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [XmlType("license")] [ProtoContract] - public class License + public class License : BomEntity { [XmlElement("id")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/LicenseChoice.cs b/src/CycloneDX.Core/Models/LicenseChoice.cs index 8d27a408..fec91a2e 100644 --- a/src/CycloneDX.Core/Models/LicenseChoice.cs +++ b/src/CycloneDX.Core/Models/LicenseChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class LicenseChoice + public class LicenseChoice : BomEntity { [XmlElement("license")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Metadata.cs b/src/CycloneDX.Core/Models/Metadata.cs index df9d7681..e6483af0 100644 --- a/src/CycloneDX.Core/Models/Metadata.cs +++ b/src/CycloneDX.Core/Models/Metadata.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Metadata + public class Metadata : BomEntity { private DateTime? _timestamp; [XmlElement("timestamp")] diff --git a/src/CycloneDX.Core/Models/Note.cs b/src/CycloneDX.Core/Models/Note.cs index feaea13e..66fd3292 100644 --- a/src/CycloneDX.Core/Models/Note.cs +++ b/src/CycloneDX.Core/Models/Note.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Note + public class Note : BomEntity { [XmlElement("locale")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalContact.cs b/src/CycloneDX.Core/Models/OrganizationalContact.cs index ab6943b1..148100e1 100644 --- a/src/CycloneDX.Core/Models/OrganizationalContact.cs +++ b/src/CycloneDX.Core/Models/OrganizationalContact.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalContact + public class OrganizationalContact : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalEntity.cs b/src/CycloneDX.Core/Models/OrganizationalEntity.cs index 4b80dd97..420333e4 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntity.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntity.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntity + public class OrganizationalEntity : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Patch.cs b/src/CycloneDX.Core/Models/Patch.cs index be9aa89f..704041a7 100644 --- a/src/CycloneDX.Core/Models/Patch.cs +++ b/src/CycloneDX.Core/Models/Patch.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Patch + public class Patch : BomEntity { [ProtoContract] public enum PatchClassification diff --git a/src/CycloneDX.Core/Models/Pedigree.cs b/src/CycloneDX.Core/Models/Pedigree.cs index 537d6e87..17609381 100644 --- a/src/CycloneDX.Core/Models/Pedigree.cs +++ b/src/CycloneDX.Core/Models/Pedigree.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Pedigree + public class Pedigree : BomEntity { [XmlArray("ancestors")] [XmlArrayItem("component")] diff --git a/src/CycloneDX.Core/Models/Property.cs b/src/CycloneDX.Core/Models/Property.cs index a61b45f2..5217e5d2 100644 --- a/src/CycloneDX.Core/Models/Property.cs +++ b/src/CycloneDX.Core/Models/Property.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Property + public class Property : BomEntity { [XmlAttribute("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ReleaseNotes.cs b/src/CycloneDX.Core/Models/ReleaseNotes.cs index b8a33ce9..d20f8d88 100644 --- a/src/CycloneDX.Core/Models/ReleaseNotes.cs +++ b/src/CycloneDX.Core/Models/ReleaseNotes.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ReleaseNotes + public class ReleaseNotes : BomEntity { [XmlElement("type")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 101e6fea..38afffcf 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Service: IEquatable + public class Service : BomEntity { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -123,7 +123,7 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } - +/* public bool Equals(Service obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -133,5 +133,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } diff --git a/src/CycloneDX.Core/Models/Source.cs b/src/CycloneDX.Core/Models/Source.cs index 1c6bbead..70c781fd 100644 --- a/src/CycloneDX.Core/Models/Source.cs +++ b/src/CycloneDX.Core/Models/Source.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Source + public class Source : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Swid.cs b/src/CycloneDX.Core/Models/Swid.cs index 4cd9eff8..84db40a2 100644 --- a/src/CycloneDX.Core/Models/Swid.cs +++ b/src/CycloneDX.Core/Models/Swid.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Swid + public class Swid : BomEntity { [XmlAttribute("tagId")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 60353ef7..8a3d5cde 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Tool: BomEntity + public class Tool : BomEntity { [XmlElement("vendor")] [ProtoMember(1)] @@ -46,17 +46,18 @@ public class Tool: BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } - +/* public bool Equals(Tool obj) { - /*return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj);*/ + //return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); return base.Equals(obj); } public override int GetHashCode() { - /*return CycloneDX.Json.Serializer.Serialize(this).GetHashCode();*/ + //return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); return base.GetHashCode(); } +*/ } } diff --git a/src/CycloneDX.Core/Models/ValidationResult.cs b/src/CycloneDX.Core/Models/ValidationResult.cs index 2580f86c..b1bb313e 100644 --- a/src/CycloneDX.Core/Models/ValidationResult.cs +++ b/src/CycloneDX.Core/Models/ValidationResult.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models /// /// The return type for all validation methods. /// - public class ValidationResult + public class ValidationResult : BomEntity { /// /// true if the document has been successfully validated. diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs index 18080bed..5e005c41 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Advisory.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Advisory + public class Advisory : BomEntity { [XmlAttribute("title")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs b/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs index f0fc31bc..c80aeca8 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/AffectedVersions.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class AffectedVersions + public class AffectedVersions : BomEntity { [XmlElement("version")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs index 9cf37d53..2e72c268 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Affects + public class Affects : BomEntity { [XmlElement("ref")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs index 7422d12a..6f494bf9 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Analysis.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Analysis + public class Analysis : BomEntity { [XmlElement("state")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs index bd9565c3..c9c59b6e 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Credits.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Credits + public class Credits : BomEntity { [XmlArray("organizations")] [XmlArrayItem("organization")] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs index acb2be22..60f90628 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Rating.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Rating + public class Rating : BomEntity { [XmlElement("source")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs index f0548218..cc2af0d7 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Reference.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Reference + public class Reference : BomEntity { [XmlAttribute("id")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index eb184534..c3daddeb 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Vulnerability: IEquatable + public class Vulnerability : BomEntity { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -125,6 +125,7 @@ public DateTime? Updated public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } +/* public bool Equals(Vulnerability obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -134,5 +135,6 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ } } From 42648fd7243a62bb0ca02b2ead50ba9fa0c8afff Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 20:56:00 +0200 Subject: [PATCH 125/285] BomEntity: forward from default Equals() and Equivalent() implems to class-customized ones if present - fix discovery, optimize GetType() call count Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index eaa59c89..086b95d8 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -200,7 +200,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equals", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -220,7 +220,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equivalent", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -240,7 +240,7 @@ public class BomEntity : IEquatable foreach (var type in KnownEntityTypes) { var method = type.GetMethod("MergeWith", - BindingFlags.Public | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[] { type }); if (method != null) dict[type] = method; @@ -263,7 +263,8 @@ internal string SerializeEntity() // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class // so comes up empty, or has to jump through hoops...) - if (KnownTypeSerializers.TryGetValue(this.GetType(), out var methodSerializeThis)) + Type thisType = this.GetType(); + if (KnownTypeSerializers.TryGetValue(thisType, out var methodSerializeThis)) { var res1 = (string)methodSerializeThis.Invoke(null, new object[] {this}); return res1; @@ -285,12 +286,13 @@ internal string SerializeEntity() /// True if two objects are deemed equal public bool Equals(BomEntity other) { - if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquals)) + Type thisType = this.GetType(); + if (KnownTypeEquals.TryGetValue(thisType, out var methodEquals)) { return (bool)methodEquals.Invoke(this, new object[] {other}); } - if (other is null || this.GetType() != other.GetType()) return false; + if (other is null || thisType != other.GetType()) return false; return this.SerializeEntity() == other.SerializeEntity(); } @@ -311,12 +313,18 @@ public override int GetHashCode() /// the same real-life entity, False otherwise. public bool Equivalent(BomEntity other) { - if (KnownTypeEquals.TryGetValue(this.GetType(), out var methodEquivalent)) + Type thisType = this.GetType(); + if (KnownTypeEquivalent.TryGetValue(thisType, out var methodEquivalent)) { + // Note we do not check for null/type of "other" at this point + // since the derived classes define the logic of equivalence + // (possibly to other entity subtypes as well). return (bool)methodEquivalent.Invoke(this, new object[] {other}); } - return (!(other is null) && (this.GetType() == other.GetType()) && this.Equals(other)); + // Note that here a default Equivalent() may call into custom Equals(), + // so the similar null/type sanity shecks are still relevant. + return (!(other is null) && (thisType == other.GetType()) && this.Equals(other)); } /// From 9484afb73d7cb26641c7da8428cba00a1f2bca85 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:27:34 +0200 Subject: [PATCH 126/285] BomEntity: use KnownTypeMergeWith[] from BomEntityListMergeHelper<> to call customized handlers where applicable Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 086b95d8..ef9da66c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -80,6 +80,10 @@ public List Merge(List list1, List list2) List result = new List(list1); Type TType = list1[0].GetType(); + if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + methodMergeWith = null; + } foreach (var item2 in list2) { @@ -102,15 +106,25 @@ public List Merge(List list1, List list2) // methods defined or inherited as is suitable for // the particular entity type, hence much less code // and error-checking than there was in the PoC: - if (item1.MergeWith(item2)) + bool resMerge; + if (methodMergeWith != null) { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + } + else + { + resMerge = item1.MergeWith(item2); } // MergeWith() may throw BomEntityConflictException which we // want to propagate to users - their input data is confusing. // Probably should not throw BomEntityIncompatibleException // unless the lists truly are of mixed types. + + if (resMerge) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } } if (isContained) @@ -153,7 +167,7 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - static List KnownEntityTypes = + public static List KnownEntityTypes = new Func>(() => { List derived_types = new List(); @@ -172,7 +186,7 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - static Dictionary KnownTypeSerializers = + public static Dictionary KnownTypeSerializers = new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); @@ -193,7 +207,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeEquals = + public static Dictionary KnownTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -213,7 +227,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeEquivalent = + public static Dictionary KnownTypeEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); @@ -233,7 +247,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - static Dictionary KnownTypeMergeWith = + public static Dictionary KnownTypeMergeWith = new Func>(() => { Dictionary dict = new Dictionary(); From 457909d6a794ead305e4efb13afe823dcf027cb4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:30:54 +0200 Subject: [PATCH 127/285] CycloneDX.Core/Models/Component.cs: basic adjustment to be a BomEntity (inherit equality methods, adapt MergeWith()) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0150e7fa..f0cd2c76 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -199,6 +199,7 @@ public bool NonNullableModified public ReleaseNotes ReleaseNotes { get; set; } public bool ShouldSerializeReleaseNotes() { return ReleaseNotes != null; } +/* public bool Equals(Component obj) { return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); @@ -208,6 +209,7 @@ public override int GetHashCode() { return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); } +*/ public bool Equivalent(Component obj) { @@ -219,13 +221,25 @@ public bool MergeWith(Component obj) if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (this.Equals(obj)) + try { - if (iDebugLevel >= 1) + // Basic checks for null, type compatibility, + // equality and non-equivalence; throws for + // the hard stuff to implement in the catch: + bool resBase = base.MergeWith(obj); + if (resBase && iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); - return true; + } + return resBase; + } + catch (BomEntityConflictException) + { + // No-op to fall through below with less indentation } + // Custom logic to squash together two equivalent entries - + // with same BomRef value but something differing elsewhere if ( (this.BomRef != null && BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) From 837bd5d3a7e7342847c6a53f7113fca4b192dea3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:34:55 +0200 Subject: [PATCH 128/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging levels for different traces Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index f0cd2c76..74cc3b2f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -249,7 +249,7 @@ public bool MergeWith(Component obj) if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly - if (iDebugLevel >= 1) + if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; @@ -270,7 +270,7 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { - if (iDebugLevel >= 2) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); switch (property.PropertyType) { @@ -304,7 +304,7 @@ public bool MergeWith(Component obj) objItem = ComponentScope.Null; } - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" @@ -312,7 +312,7 @@ public bool MergeWith(Component obj) { // keep absent==required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } @@ -323,7 +323,7 @@ public bool MergeWith(Component obj) ) { // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); continue; } @@ -345,7 +345,7 @@ public bool MergeWith(Component obj) bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); if (objItem) property.SetValue(tmp, true); @@ -359,7 +359,7 @@ public bool MergeWith(Component obj) var propValObj = property.GetValue(obj); if (propValTmp == null && propValObj == null) { - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); continue; } @@ -378,7 +378,7 @@ public bool MergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); if (propValObj == null || propValObjCount < 1) @@ -416,7 +416,7 @@ public bool MergeWith(Component obj) { if (methodEquals != null) { - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); propsSeemEqualLearned = true; @@ -425,7 +425,7 @@ public bool MergeWith(Component obj) catch (System.Exception exc) { // no-op - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } @@ -439,21 +439,21 @@ public bool MergeWith(Component obj) { try { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) mergedOk = false; } catch (System.Exception exc) { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { - if (iDebugLevel >= 6) + if (iDebugLevel >= 7) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); } } // else: tmpitem considered not equal, should be added @@ -479,12 +479,12 @@ public bool MergeWith(Component obj) // e.g. 'Scope' helper // followed by 'Scope' // which we specially handle above - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); continue; } - if (iDebugLevel >= 3) + if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); @@ -508,7 +508,7 @@ public bool MergeWith(Component obj) { if (methodEquals != null) { - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): try methodEquals()"); propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); propsSeemEqualLearned = true; @@ -517,7 +517,7 @@ public bool MergeWith(Component obj) catch (System.Exception exc) { // no-op - if (iDebugLevel >= 5) + if (iDebugLevel >= 6) Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); } @@ -526,7 +526,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; @@ -542,7 +542,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqualLearned) { // Fall back to generic equality check which may be useless - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; @@ -555,7 +555,7 @@ public bool MergeWith(Component obj) if (!propsSeemEqual) { - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } @@ -572,7 +572,7 @@ public bool MergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - if (iDebugLevel >= 4) + if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); mergedOk = false; } @@ -582,7 +582,7 @@ public bool MergeWith(Component obj) // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) continue; - if (iDebugLevel >= 6) + if (iDebugLevel >= 7) Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); mergedOk = false; } From 4d30059e7c46988f627eaa6ed698813629518b65 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 21:59:22 +0200 Subject: [PATCH 129/285] CycloneDX.Core/Models/Component.cs: MergeWith(): speed-up with BomEntity.KnownEntityTypeProperties[] Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 16 ++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ef9da66c..fe517e8d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -181,6 +181,22 @@ public class BomEntity : IEquatable return derived_types; }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom Equals() method implementations + /// (if present), prepared startically at start time. + /// + public static Dictionary KnownEntityTypeProperties = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + dict[type] = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 74cc3b2f..2c8dceaa 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -248,7 +248,7 @@ public bool MergeWith(Component obj) // merge the attribute values with help of reflection: if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); - PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; //this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); From 8801c514ca9ddbcc494a6a77a377d196473f3e39 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:01:45 +0200 Subject: [PATCH 130/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 2c8dceaa..7bad243f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -227,9 +227,16 @@ public bool MergeWith(Component obj) // equality and non-equivalence; throws for // the hard stuff to implement in the catch: bool resBase = base.MergeWith(obj); - if (resBase && iDebugLevel >= 1) + if (iDebugLevel >= 1) { - Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); + if (resBase) + { + Console.WriteLine($"Component.MergeWith(): SKIP: contents are identical, nothing to do"); + } + else + { + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + } } return resBase; } From 693a8e7df59523727c36dfb1c43e2211ce6b21c2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:30:55 +0200 Subject: [PATCH 131/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 7bad243f..44ccd21c 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -235,7 +235,8 @@ public bool MergeWith(Component obj) } else { - Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); } } return resBase; From a330604269b9accb4d2c658634a5d76db9a55521 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:31:55 +0200 Subject: [PATCH 132/285] CycloneDX.Core/Models/Component.cs: rearrange processing of un-set (null) Scope value Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 44ccd21c..c3d93fdc 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -118,6 +118,8 @@ public ComponentScope NonNullableScope { get { + if (Scope == null) + return ComponentScope.Null; return Scope.Value; } set @@ -296,9 +298,14 @@ public bool MergeWith(Component obj) { tmpItem = (ComponentScope)property.GetValue(tmp, null); } - catch (System.Exception) + catch (System.InvalidOperationException) { // Unspecified => required per CycloneDX spec v1.4?.. + // Currently handled below like that, so (enum) Null value here. + tmpItem = ComponentScope.Null; + } + catch (System.Reflection.TargetInvocationException) + { tmpItem = ComponentScope.Null; } @@ -307,7 +314,11 @@ public bool MergeWith(Component obj) { objItem = (ComponentScope)property.GetValue(obj, null); } - catch (System.Exception) + catch (System.InvalidOperationException) + { + objItem = ComponentScope.Null; + } + catch (System.Reflection.TargetInvocationException) { objItem = ComponentScope.Null; } From 80b8c46631a7acd2cfbe6f868200d57f4f216dd4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 22:40:24 +0200 Subject: [PATCH 133/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise logging of skipping Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c3d93fdc..53de54b8 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -625,7 +625,7 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related upon second look"); } // Merge was not applicable or otherwise did not succeed From 4f19ce1f5f7e211335e185157810fd1aef395a22 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 23:01:56 +0200 Subject: [PATCH 134/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise handling of various Scope values, refresh comments about spec versions involved Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 53de54b8..13ba2d01 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -251,7 +251,7 @@ public bool MergeWith(Component obj) // Custom logic to squash together two equivalent entries - // with same BomRef value but something differing elsewhere if ( - (this.BomRef != null && BomRef.Equals(obj.BomRef)) || + (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) ) { // Objects seem equivalent according to critical arguments; @@ -326,16 +326,38 @@ public bool MergeWith(Component obj) if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); - // Per CycloneDX spec v1.4, absent value "SHOULD" be treated as "required" + // Since CycloneDX spec v1.0 up to at least v1.4, + // an absent value "SHOULD" be treated as "required" if (tmpItem != ComponentScope.Excluded && objItem != ComponentScope.Excluded) { - // keep absent==required; upgrade optional objItem + // BOTH are not specified + if (tmpItem == ComponentScope.Null && objItem == ComponentScope.Null) + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SCOPE: keep unspecified explicitly"); + continue; + } + + if (tmpItem == ComponentScope.Optional && objItem == ComponentScope.Optional) + { + property.SetValue(tmp, ComponentScope.Optional); + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): SCOPE: keep 'Optional'"); + continue; + } + + // Any one (or both) are Required, or Null meaning required: + // keep absent=>required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 4) Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); continue; } + // NOTE: "excluded" is only defined since CycloneDX spec v1.1 => + // you should not see it read from v1.0 documents. + // TOTHINK: Theoretically: what if we are asked to output a v1.0 + // document after merge of newer documents? Emitter should care... if ( (tmpItem == ComponentScope.Excluded && objItem == ComponentScope.Optional) || (objItem == ComponentScope.Excluded && tmpItem == ComponentScope.Optional) From a37a6e41ef78237127657d3840551326a828ce05 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 9 Aug 2023 23:26:09 +0200 Subject: [PATCH 135/285] CycloneDX.Core/Models/Component.cs: MergeWith(): revise discovery of methodEquals and methodMergeWith via pre-cached BomEntity static Dicts Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 21 ++++++++++++ src/CycloneDX.Core/Models/Component.cs | 45 +++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index fe517e8d..0bf4bc5a 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -238,6 +238,27 @@ public class BomEntity : IEquatable return dict; }) (); + // Our loops check for some non-BomEntity typed value equalities, + // so cache their methods if present. Note that this one retains + // the "null" results to mark that we do not need to look further. + public static Dictionary KnownOtherTypeEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var listMore = new List(); + listMore.Add(typeof(string)); + listMore.Add(typeof(bool)); + listMore.Add(typeof(int)); + foreach (var type in listMore) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + dict[type] = method; + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about their custom Equivalent() method implementations diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 13ba2d01..9996c703 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -434,8 +434,26 @@ public bool MergeWith(Component obj) } var TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + // No need to re-query now that we have BomEntity descendance: + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + methodMergeWith = null; + } + + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) + { + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + { + methodEquals = methodEquals2; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } + } for (int o = 0; o < propValObjCount; o++) { @@ -541,7 +559,20 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); - var methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) + { + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + { + methodEquals = methodEquals2; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } + } + bool propsSeemEqual = false; bool propsSeemEqualLearned = false; @@ -600,7 +631,13 @@ public bool MergeWith(Component obj) Console.WriteLine($"Component.MergeWith(): items say they are not equal"); } - var methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) + { + // No need to re-query now that we have BomEntity descendance: + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + methodMergeWith = null; + } + if (methodMergeWith != null) { try From c538a3eb7d74f1ad466680b47716a9cfc08877ed Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 00:27:24 +0200 Subject: [PATCH 136/285] Component.MergeWith() and ListMergeHelper.Merge(): use cached BomEntityListMergeHelperReflection and BomEntityListReflection Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 27 ++++++--- src/CycloneDX.Core/Models/BomEntity.cs | 83 ++++++++++++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 22 ++++++- 3 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index e9ab7adb..09ba178c 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Text.RegularExpressions; using CycloneDX.Models; @@ -43,13 +44,25 @@ public List Merge(List list1, List list2) if (typeof(BomEntity).IsInstanceOfType(list1[0])) { - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + MethodInfo methodMerge = null; + Object helper; + // Use cached info where available + if (BomEntity.KnownBomEntityListMergeHelpers.TryGetValue(typeof(T), out BomEntityListMergeHelperReflection refInfo)) + { + methodMerge = refInfo.methodMerge; + helper = refInfo.helperInstance; + } + else + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); + helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + } + if (methodMerge != null) { return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0bf4bc5a..cbf39e3f 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -146,6 +146,22 @@ public List Merge(List list1, List list2) } } + public class BomEntityListReflection + { + public Type genericType; + public PropertyInfo propCount; + public MethodInfo methodAdd; + public MethodInfo methodAddRange; + public MethodInfo methodGetItem; + } + + public class BomEntityListMergeHelperReflection + { + public Type genericType; + public MethodInfo methodMerge; + public Object helperInstance; + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -197,6 +213,73 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownEntityTypeLists = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listType = typeof(List<>); + Type constructedListType = listType.MakeGenericType(type); + // Needed? var helper = Activator.CreateInstance(constructedListType); + + dict[type] = new BomEntityListReflection(); + dict[type].genericType = constructedListType; + + // Gotta use reflection for run-time evaluated type methods: + dict[type].propCount = constructedListType.GetProperty("Count"); + dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); + dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); + dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + } + return dict; + }) (); + + public static Dictionary KnownBomEntityListMergeHelpers = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + Type constructedListHelperType = listHelperType.MakeGenericType(type); + var helper = Activator.CreateInstance(constructedListHelperType); + Type LType = null; + if (KnownEntityTypeLists.TryGetValue(type, out BomEntityListReflection refInfo)) + { + LType = refInfo.genericType; + } + + if (LType != null) + { + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType }); + if (methodMerge != null) + { + dict[type] = new BomEntityListMergeHelperReflection(); + dict[type].genericType = constructedListHelperType; + dict[type].methodMerge = methodMerge; + dict[type].helperInstance = helper; + // Callers would return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - make noise + throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a Merge() helper method"); + } + } + else + { + throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a List class definition"); + } + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 9996c703..f0005d66 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -406,9 +406,25 @@ public bool MergeWith(Component obj) } var LType = (propValTmp == null ? propValObj.GetType() : propValTmp.GetType()); - var propCount = LType.GetProperty("Count"); - var methodGetItem = LType.GetMethod("get_Item"); - var methodAdd = LType.GetMethod("Add"); + // Use cached info where available + PropertyInfo propCount = null; + MethodInfo methodGetItem = null; + MethodInfo methodAdd = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(LType, out BomEntityListReflection refInfo)) + { + propCount = refInfo.propCount; + methodGetItem = refInfo.methodGetItem; + methodAdd = refInfo.methodAdd; + } + else + { + if (iDebugLevel >= 4) + Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); + propCount = LType.GetProperty("Count"); + methodGetItem = LType.GetMethod("get_Item"); + methodAdd = LType.GetMethod("Add"); + } + if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) From 3f8b8aac9e0e6883e8a669030c516bd5e4637c8b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:14:32 +0200 Subject: [PATCH 137/285] Component.MergeWith(): handle Enum values quickly Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index f0005d66..45bc1c5c 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -575,6 +575,18 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); + + if (TType.IsEnum) + { + if (propValTmp == propValObj) + { + continue; + } + + mergedOk = false; + break; + } + if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) From fbe83b1d1faa71b7e5438c672d68216e710a5edc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:17:56 +0200 Subject: [PATCH 138/285] Component.MergeWith(): handle Enum values even more quickly Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 45bc1c5c..1941eaa6 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -544,6 +544,21 @@ public bool MergeWith(Component obj) } break; + // Default handling for enums, if not customized above + case Type _ when (property.PropertyType.IsEnum): + { + // Not nullable! + var propValTmp = property.GetValue(tmp, null); + var propValObj = property.GetValue(obj, null); + if (propValTmp == propValObj) + { + continue; + } + + mergedOk = false; + } + break; + default: { if ( @@ -575,18 +590,6 @@ public bool MergeWith(Component obj) } var TType = propValTmp.GetType(); - - if (TType.IsEnum) - { - if (propValTmp == propValObj) - { - continue; - } - - mergedOk = false; - break; - } - if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) From 51e7eac4ca07946cceba6a9844643ee2316376d3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 01:34:55 +0200 Subject: [PATCH 139/285] Component.MergeWith(): try to avoid spurious values for null (missing in original JSON) NonNullable properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 1941eaa6..e56880d0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -271,6 +271,11 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { try { + // Avoid spurious "modified=false" in merged JSON + if (property.Name == "Modified" && !(this.Modified.HasValue)) + continue; + if (property.Name == "Scope" && !(this.Scope.HasValue)) + continue; property.SetValue(tmp, property.GetValue(this, null)); } catch (System.Exception) { // no-op From 1c8c5a836f94dd34291789c3d971efed230dc703 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:12:35 +0200 Subject: [PATCH 140/285] Component.MergeWith(): keep track of BomEntity-derived classes which use default Equals() and Equivalent() implementations Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 36 ++++++++++++++++++++++++++ src/CycloneDX.Core/Models/Component.cs | 33 ++++++++++++++++------- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cbf39e3f..8a6cae14 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -321,6 +321,24 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownDefaultEquals = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var methodDefault = typeof(BomEntity).GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { typeof(BomEntity) }); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equals", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + if (method == null) + dict[type] = methodDefault; + } + return dict; + }) (); + // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. @@ -362,6 +380,24 @@ public class BomEntity : IEquatable return dict; }) (); + public static Dictionary KnownDefaultEquivalent = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var methodDefault = typeof(BomEntity).GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { typeof(BomEntity) }); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("Equivalent", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] { type }); + if (method == null) + dict[type] = methodDefault; + } + return dict; + }) (); + /// /// Dictionary mapping classes derived from BomEntity to reflection /// MethodInfo about their custom MergeWith() method implementations diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index e56880d0..2fddced0 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -423,7 +423,7 @@ public bool MergeWith(Component obj) } else { - if (iDebugLevel >= 4) + if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); propCount = LType.GetProperty("Count"); methodGetItem = LType.GetMethod("get_Item"); @@ -464,15 +464,22 @@ public bool MergeWith(Component obj) if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { - if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + if (KnownDefaultEquals.TryGetValue(TType, out var methodEquals2)) { methodEquals = methodEquals2; } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); - if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals3)) + { + methodEquals = methodEquals3; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } @@ -597,18 +604,26 @@ public bool MergeWith(Component obj) var TType = propValTmp.GetType(); if (!KnownTypeEquals.TryGetValue(TType, out var methodEquals)) { - if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals2)) + if (KnownDefaultEquals.TryGetValue(TType, out var methodEquals2)) { methodEquals = methodEquals2; } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); - if (iDebugLevel >= 1) - Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + if (KnownOtherTypeEquals.TryGetValue(TType, out var methodEquals3)) + { + methodEquals = methodEquals3; + } + else + { + methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + if (iDebugLevel >= 1) + Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } + bool propsSeemEqual = false; bool propsSeemEqualLearned = false; From 5f3b30be98a6e9711d64422b7f69a22ee2807080 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:21:21 +0200 Subject: [PATCH 141/285] BomEntity: when tracking BomEntityListReflection[type] also leave a key for BomEntityListReflection[List] Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 8a6cae14..e48f1778 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -233,6 +233,10 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + + // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] + // TODO: Separate dict?.. + dict[constructedListType] = dict[type]; } return dict; }) (); From c7d2f410b766999f8eb2464c8d755d2dc7bc14a3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:36:40 +0200 Subject: [PATCH 142/285] Component.MergeWith(): skip changes due to un-set "other" Modified property Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 2fddced0..e8dcdd0e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -387,7 +387,9 @@ public bool MergeWith(Component obj) case Type _ when (property.Name == "NonNullableModified"): { - // Not nullable! + // Not nullable! Keep un-set if applicable. + if (!obj.Modified.HasValue) + continue; bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); From ddc71d2f28a41ca9647b0c9988dfd3f11ec8a8cb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 02:39:40 +0200 Subject: [PATCH 143/285] Component.MergeWith(): skip changes due to un-set "this+other" Scope property Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index e8dcdd0e..ccd5bda1 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -297,7 +297,10 @@ public bool MergeWith(Component obj) */ case Type _ when property.PropertyType == typeof(ComponentScope): { - // Not nullable! + // Not nullable! Quickly keep un-set if applicable. + if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) + continue; + ComponentScope tmpItem; try { From 5701a1cf92b1e893db6f979733e47cd2ae2867af Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 11:56:17 +0200 Subject: [PATCH 144/285] Component.MergeWith(): update comments about Scope and NonNullableScope Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index ccd5bda1..0b352a9e 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -297,6 +297,7 @@ public bool MergeWith(Component obj) */ case Type _ when property.PropertyType == typeof(ComponentScope): { + // NOTE: Intentionally not matching 'Scope' helper // Not nullable! Quickly keep un-set if applicable. if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) continue; @@ -584,7 +585,7 @@ public bool MergeWith(Component obj) ) { // e.g. 'Scope' helper - // followed by 'Scope' + // followed by '{ComponentScope NonNullableScope}' // which we specially handle above if (iDebugLevel >= 5) Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); From ce4df43ce51b9c3819b8e644a04aa3bf8a0206db Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 11:57:36 +0200 Subject: [PATCH 145/285] Component.MergeWith(): interrupt list processing as soon as we have a hit (handled a "tmp" target entry which was equivalent or equal to an incoming "obj" entry) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0b352a9e..c2d49e80 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -496,7 +496,7 @@ public bool MergeWith(Component obj) continue; bool listHit = false; - for (int t = 0; t < propValTmpCount; t++) + for (int t = 0; t < propValTmpCount && !listHit; t++) { var tmpItem = methodGetItem.Invoke(propValTmp, new object[] { t }); if (tmpItem != null) From 993903db03c6c5a46c70aaec77f8d748132ce724 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:13:01 +0200 Subject: [PATCH 146/285] Component.MergeWith(): when preparing the "tmp" clone, skip "NonNullable" helper properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c2d49e80..cb825b71 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -272,10 +272,15 @@ public bool MergeWith(Component obj) { try { // Avoid spurious "modified=false" in merged JSON - if (property.Name == "Modified" && !(this.Modified.HasValue)) + // Also skip helpers, care about real values + if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(this.Modified.HasValue)) { + // Can not set R/O prop: ### tmp.Modified.HasValue = false; continue; - if (property.Name == "Scope" && !(this.Scope.HasValue)) + } + if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(this.Scope.HasValue)) { + // Can not set R/O prop: ### tmp.Scope.HasValue = false; continue; + } property.SetValue(tmp, property.GetValue(this, null)); } catch (System.Exception) { // no-op From f72f6b595f88a5e02cde1d7841ee616277768d31 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:13:55 +0200 Subject: [PATCH 147/285] Component.MergeWith(): when copying back from the "tmp" clone to "this", skip "NonNullable" helper properties Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index cb825b71..d5cb6f41 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -735,6 +735,16 @@ public bool MergeWith(Component obj) // No failures, only now update the current object: foreach (PropertyInfo property in properties) { + // Avoid spurious "modified=false" in merged JSON + // Also skip helpers, care about real values + if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(tmp.Modified.HasValue)) { + // Can not set R/O prop: ### this.Modified.HasValue = false; + continue; + } + if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(tmp.Scope.HasValue)) { + // Can not set R/O prop: ### this.Scope.HasValue = false; + continue; + } property.SetValue(this, property.GetValue(tmp, null)); } } From 50dea1f1f5493d58adaa113da3b70ebadc641c95 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 12:41:18 +0200 Subject: [PATCH 148/285] Component.MergeWith(): leave a TODO comment for code sharing eventually Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index d5cb6f41..fba5721b 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -250,6 +250,11 @@ public bool MergeWith(Component obj) // Custom logic to squash together two equivalent entries - // with same BomRef value but something differing elsewhere + // TODO: Much of this seems reusable - if other classes get + // a need for some fully-fledged MergeWith, consider breaking + // this code into helper methods and patterns, so that only + // specific property hits would be customized and the default + // scaffolding shared. if ( (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) From d93e2d21aae4eea6a6b77e60ff95d981b368eb90 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 18:48:47 +0200 Subject: [PATCH 149/285] CycloneDX.Core/Json/Serializer.Serialization.cs: implement the one Serialize(BomEntity) to rule them all Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index 2348fc9d..d99f4500 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -61,7 +61,43 @@ public static string Serialize(Bom bom) internal static string Serialize(BomEntity entity) { Contract.Requires(entity != null); - return JsonSerializer.Serialize(entity, _options); + // Default code tends to return serialization of base class + // => empty (no props in BomEntity itself) so we have to + // coerce it into seeing the object type we need to parse. + string res = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(entity.GetType(), out var listInfo) + && listInfo != null && listInfo.genericType != null + && listInfo.methodAdd != null && listInfo.methodGetItem != null + ) { + var castList = Activator.CreateInstance(listInfo.genericType); + listInfo.methodAdd.Invoke(castList, new object[] { entity }); + res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), _options); + } + else + { + var castEntity = Convert.ChangeType(entity, entity.GetType()); +/* + // Inspired by https://stackoverflow.com/a/4661237/4715872 + // to craft a List "result" at run-time: + Type listHelperType = typeof(BomEntityListMergeHelper<>); + var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); + var helper = Activator.CreateInstance(constructedListHelperType); + // Gotta use reflection for run-time evaluated type methods: + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + if (methodMerge != null) + { + return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + } + else + { + // Should not get here, but if we do - log and fall through + if (iDebugLevel >= 1) + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } +*/ + res = JsonSerializer.Serialize(castEntity, _options); + } + return res; } internal static string Serialize(Component component) From 182b9cf66704d32f9f01a98958996859e4fcf2e1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 18:49:24 +0200 Subject: [PATCH 150/285] CycloneDX.Core/Json/Serializer.Serialization.cs: comment away other internal Serialize() implementations Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Serializer.Serialization.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index d99f4500..a3742df6 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -99,7 +99,7 @@ internal static string Serialize(BomEntity entity) } return res; } - +/* internal static string Serialize(Component component) { Contract.Requires(component != null); @@ -129,5 +129,6 @@ internal static string Serialize(Models.Vulnerabilities.Vulnerability vulnerabil Contract.Requires(vulnerability != null); return JsonSerializer.Serialize(vulnerability, _options); } +*/ } } From 205c986e05a01bdb6e496274e3dbce26dd1cafa8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:33:16 +0200 Subject: [PATCH 151/285] CycloneDX.Core/Json/Serializer.Serialization.cs: tidy up Serialize(BomEntity implem) Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index a3742df6..21c8ddbb 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -58,12 +58,20 @@ public static string Serialize(Bom bom) return jsonBom; } + /// + /// Return serialization of a class derived from BomEntity. + /// + /// A BomEntity-derived class + /// String with JSON markup internal static string Serialize(BomEntity entity) { Contract.Requires(entity != null); // Default code tends to return serialization of base class // => empty (no props in BomEntity itself) so we have to // coerce it into seeing the object type we need to parse. + // This codepath is critical for us since serialization is + // used to compare if entities are Equal() in massive loops + // when merging Bom's. Optimizations welcome. string res = null; if (BomEntity.KnownEntityTypeLists.TryGetValue(entity.GetType(), out var listInfo) && listInfo != null && listInfo.genericType != null @@ -76,29 +84,11 @@ internal static string Serialize(BomEntity entity) else { var castEntity = Convert.ChangeType(entity, entity.GetType()); -/* - // Inspired by https://stackoverflow.com/a/4661237/4715872 - // to craft a List "result" at run-time: - Type listHelperType = typeof(BomEntityListMergeHelper<>); - var constructedListHelperType = listHelperType.MakeGenericType(list1[0].GetType()); - var helper = Activator.CreateInstance(constructedListHelperType); - // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); - if (methodMerge != null) - { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); - } - else - { - // Should not get here, but if we do - log and fall through - if (iDebugLevel >= 1) - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); - } -*/ res = JsonSerializer.Serialize(castEntity, _options); } return res; } + /* internal static string Serialize(Component component) { From ad26001683fb83c2fe726978930b8f06a11c9c3a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:47:17 +0200 Subject: [PATCH 152/285] CycloneDX.Core/Json/Serializer.Serialization.cs: introduce SerializeCompact(BomEntity) with minimal markup overhead and use it in BomEntity.Equals() Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 39 +++++++++++++++++-- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index 21c8ddbb..d7cc2fc1 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -33,6 +33,14 @@ namespace CycloneDX.Json public static partial class Serializer { private static JsonSerializerOptions _options = Utils.GetJsonSerializerOptions(); + private static readonly JsonSerializerOptions _options_compact = new Func(() => + { + JsonSerializerOptions opts = Utils.GetJsonSerializerOptions(); + opts.AllowTrailingCommas = false; + opts.WriteIndented = false; + + return opts; + }) (); /// /// Serializes a CycloneDX BOM writing the output to a stream. @@ -59,11 +67,36 @@ public static string Serialize(Bom bom) } /// - /// Return serialization of a class derived from BomEntity. + /// Return serialization of a class derived from BomEntity + /// with common JsonSerializerOptions defined for this class. /// /// A BomEntity-derived class /// String with JSON markup internal static string Serialize(BomEntity entity) + { + return Serialize(entity, _options); + } + + /// + /// Return serialization of a class derived from BomEntity + /// with compact JsonSerializerOptions aimed at minimal + /// markup (harder to read for humans, less bytes to parse). + /// + /// A BomEntity-derived class + /// String with JSON markup + internal static string SerializeCompact(BomEntity entity) + { + return Serialize(entity, _options_compact); + } + + /// + /// Return serialization of a class derived from BomEntity + /// with caller-specified JsonSerializerOptions. + /// + /// A BomEntity-derived class + /// Options for serializer + /// String with JSON markup + internal static string Serialize(BomEntity entity, JsonSerializerOptions jserOptions) { Contract.Requires(entity != null); // Default code tends to return serialization of base class @@ -79,12 +112,12 @@ internal static string Serialize(BomEntity entity) ) { var castList = Activator.CreateInstance(listInfo.genericType); listInfo.methodAdd.Invoke(castList, new object[] { entity }); - res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), _options); + res = JsonSerializer.Serialize(listInfo.methodGetItem.Invoke(castList, new object[] { 0 }), jserOptions); } else { var castEntity = Convert.ChangeType(entity, entity.GetType()); - res = JsonSerializer.Serialize(castEntity, _options); + res = JsonSerializer.Serialize(castEntity, jserOptions); } return res; } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e48f1778..6bde6c97 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -444,7 +444,7 @@ internal string SerializeEntity() return res1; } - var res = CycloneDX.Json.Serializer.Serialize(this); + var res = CycloneDX.Json.Serializer.SerializeCompact(this); return res; } From a7284492aa405789141d9ffd5c3222eed60b69f9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 19:53:34 +0200 Subject: [PATCH 153/285] BomEntity: avoid caching in KnownTypeSerializers any default implementations for BomEntity itself as the handler for derived classes Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 6bde6c97..26dca1e6 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -293,13 +293,16 @@ public class BomEntity : IEquatable new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); + var methodDefault = jserClassType.GetMethod("Serialize", + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + new Type[] { typeof(BomEntity) }); Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { var method = jserClassType.GetMethod("Serialize", - BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, new Type[] { type }); - if (method != null) + if (method != null && method != methodDefault) dict[type] = method; } return dict; From 3d46dbc4f7d307bb0697eebceb1a5fd0fca9c68b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:17:36 +0200 Subject: [PATCH 154/285] Component.MergeWith(): fix comparison of enums Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index fba5721b..28f10735 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -578,7 +578,7 @@ public bool MergeWith(Component obj) // Not nullable! var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); - if (propValTmp == propValObj) + if (propValTmp == propValObj || propValTmp.Equals(propValObj)) { continue; } From 43a72f17589378d861dde77ca1257b6a4efea0fb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:17:54 +0200 Subject: [PATCH 155/285] Component.MergeWith(): optimize detection of Nullable a bit Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 28f10735..4b8cbf90 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -591,6 +591,7 @@ public bool MergeWith(Component obj) { if ( /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ + property.PropertyType.Name.StartsWith("Nullable") || property.PropertyType.ToString().StartsWith("System.Nullable") ) { From c8b704e1a9ff032f007208418283746af73e7a9d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 21:29:43 +0200 Subject: [PATCH 156/285] Component.MergeWith(): fix comparison of stubborn enums Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 4b8cbf90..96e394c8 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -578,7 +578,8 @@ public bool MergeWith(Component obj) // Not nullable! var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); - if (propValTmp == propValObj || propValTmp.Equals(propValObj)) + // For some reason, reflected enums do not like getting compared + if (propValTmp == propValObj || propValTmp.Equals(propValObj) || ((Enum)propValTmp).CompareTo((Enum)propValObj) == 0 || propValTmp.ToString().Equals(propValObj.ToString())) { continue; } From ffb310861b7ae4a32665dce6cc59a42322819086 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 23:28:20 +0200 Subject: [PATCH 157/285] Introduce BomEntityListMergeHelperStrategy to tweak run-time behaviors Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 7 ++++- src/CycloneDX.Core/Models/BomEntity.cs | 38 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 09ba178c..d426139d 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -35,6 +35,11 @@ namespace CycloneDX public class ListMergeHelper { public List Merge(List list1, List list2) + { + return Merge(list1, list2, BomEntityListMergeHelperStrategy.Default()); + } + + public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -42,7 +47,7 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (typeof(BomEntity).IsInstanceOfType(list1[0])) + if (listMergeHelperStrategy.useBomEntityMerge && typeof(BomEntity).IsInstanceOfType(list1[0])) { MethodInfo methodMerge = null; Object helper; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 26dca1e6..789edb8c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -64,6 +64,44 @@ public BomEntityIncompatibleException(string msg, Type type1, Type type2) { } } + /// + /// Global configuration helper for ListMergeHelper, + /// BomEntityListMergeHelper, Merge.cs implementations + /// and related codebase. + /// + public class BomEntityListMergeHelperStrategy + { + /// + /// Cause ListMergeHelper to consider calling + /// the BomEntityListMergeHelper->Merge which in + /// turn calls BomEntity->MergeWith() in a loop, + /// vs. just comparing entities for equality and + /// deduplicating based on that (goes faster but + /// may cause data structure not conforming to spec) + /// + public bool useBomEntityMerge; + + + /// + /// CycloneDX spec version. + /// + public SpecificationVersion specificationVersion; + + /// + /// Return reasonable default strategy settings. + /// + /// A new ListMergeHelperStrategy instance + /// which the callers can tune to their liking. + public static BomEntityListMergeHelperStrategy Default() + { + return new BomEntityListMergeHelperStrategy() + { + useBomEntityMerge = true, + specificationVersion = SpecificationVersionHelpers.CurrentVersion + }; + } + } + public class BomEntityListMergeHelper where T : BomEntity { public List Merge(List list1, List list2) From 45c30f22471f0eded45b46e5c5006f1ad54786c5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 10 Aug 2023 23:44:14 +0200 Subject: [PATCH 158/285] Merge.cs: Use BomEntityListMergeHelperStrategy for quick FlatMerge() over the big population of Boms, and a clean-up pass in the end to deduplicate equivalent but unequal entries Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 44 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 184c9239..c35008d6 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -51,6 +51,11 @@ public static partial class CycloneDXUtils /// /// public static Bom FlatMerge(Bom bom1, Bom bom2) + { + return FlatMerge(bom1, bom2, BomEntityListMergeHelperStrategy.Default()); + } + + public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; @@ -67,14 +72,14 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) }; var toolsMerger = new ListMergeHelper(); - var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools); + var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools, listMergeHelperStrategy); if (tools != null) { result.Metadata.Tools = tools; } var componentsMerger = new ListMergeHelper(); - result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); + result.Components = componentsMerger.Merge(bom1.Components, bom2.Components, listMergeHelperStrategy); // Add main component from bom2 as a "yet another component" // if missing in that list so far. Note: any more complicated @@ -110,19 +115,19 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) } var servicesMerger = new ListMergeHelper(); - result.Services = servicesMerger.Merge(bom1.Services, bom2.Services); + result.Services = servicesMerger.Merge(bom1.Services, bom2.Services, listMergeHelperStrategy); var extRefsMerger = new ListMergeHelper(); - result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences); + result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences, listMergeHelperStrategy); var dependenciesMerger = new ListMergeHelper(); - result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies); + result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies, listMergeHelperStrategy); var compositionsMerger = new ListMergeHelper(); - result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions); + result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions, listMergeHelperStrategy); var vulnerabilitiesMerger = new ListMergeHelper(); - result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities); + result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities, listMergeHelperStrategy); result = CleanupMetadataComponent(result); result = CleanupEmptyLists(result); @@ -165,17 +170,31 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); + BomEntityListMergeHelperStrategy quickStrategy = BomEntityListMergeHelperStrategy.Default(); + quickStrategy.useBomEntityMerge = false; // Note: we were asked to "merge" and so we do, per principle of // least surprise - even if there is just one entry in boms[] so // we might be inclined to skip the loop. Resulting document WILL // differ from such single original (serialNumber, timestamp...) + int countBoms = 0; foreach (var bom in boms) { - result = FlatMerge(result, bom); + result = FlatMerge(result, bom, quickStrategy); + countBoms++; } - if (bomSubject != null) + // The quickly-made merged Bom is likely messy (only deduplicating + // identical entries). Run another merge, careful this time, over + // the resulting collection with a lot fewer items to inspect with + // the heavier logic. + if (bomSubject is null) + { + var emptyBom = new Bom(); + result = FlatMerge(emptyBom, result, safeStrategy); + } + else { // use the params provided if possible: prepare a new document // with desired "metadata/component" and merge differing data @@ -184,12 +203,15 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) resultSubj.Metadata.Component = bomSubject; resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); - result = FlatMerge(resultSubj, result); + result = FlatMerge(resultSubj, result, safeStrategy); var mainDependency = new Dependency(); mainDependency.Ref = result.Metadata.Component.BomRef; mainDependency.Dependencies = new List(); - + + // Revisit original Boms which had a metadata/component + // to write them up as dependencies of newly injected + // top-level product name. foreach (var bom in boms) { if (!(bom.Metadata?.Component is null)) From 48fcc03a7254bf0053d9747c41b8dd3356aa5e8a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 00:04:32 +0200 Subject: [PATCH 159/285] Merge.cs, BomEntity.cs: implement BomEntityListMergeHelperStrategy for quick and careless merges in BomEntityListMergeHelper class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 6 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 44 ++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index d426139d..ab0cd623 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -47,7 +47,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; - if (listMergeHelperStrategy.useBomEntityMerge && typeof(BomEntity).IsInstanceOfType(list1[0])) + if (typeof(BomEntity).IsInstanceOfType(list1[0])) { MethodInfo methodMerge = null; Object helper; @@ -65,12 +65,12 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: - methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List) }); + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); } if (methodMerge != null) { - return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + return (List)methodMerge.Invoke(helper, new object[] {list1, list2, listMergeHelperStrategy}); } else { diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 789edb8c..a5374f4b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -104,7 +104,7 @@ public static BomEntityListMergeHelperStrategy Default() public class BomEntityListMergeHelper where T : BomEntity { - public List Merge(List list1, List list2) + public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { //return BomUtils.MergeBomEntityLists(list1, list2); if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) @@ -113,8 +113,46 @@ public List Merge(List list1, List list2) if (list1 is null || list1.Count < 1) return list2; if (list2 is null || list2.Count < 1) return list1; + if (!listMergeHelperStrategy.useBomEntityMerge) + { + // Most BomEntity classes are not individually IEquatable to avoid the + // copy-paste coding overhead, however they inherit the Equals() and + // GetHashCode() methods from their base class. + if (iDebugLevel >= 1) + Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + + List hashList = new List(); + List resultQ = new List(); + + // Exclude possibly pre-existing identical entries first, then similarly + // handle data from the second list. Here we have the "benefit" of lack + // of real content merging, so already saved items (and their hashes) + // can be treated as immutable. + foreach (T item1 in list1) + { + if (item1 is null) continue; + int hash1 = item1.GetHashCode(); + if (hashList.Contains(hash1)) + continue; + resultQ.Add(item1); + hashList.Add(hash1); + } + + foreach (T item2 in list2) + { + if (item2 is null) continue; + int hash2 = item2.GetHashCode(); + if (hashList.Contains(hash2)) + continue; + resultQ.Add(item2); + hashList.Add(hash2); + } + + return resultQ; + } + if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for BomEntity derivatives: {list1.GetType().ToString()}"); + Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {list1.GetType().ToString()}"); List result = new List(list1); Type TType = list1[0].GetType(); @@ -299,7 +337,7 @@ public class BomEntity : IEquatable if (LType != null) { // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType }); + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); if (methodMerge != null) { dict[type] = new BomEntityListMergeHelperReflection(); From 483689ef39760683fdff31535077c95bb25a7e62 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 01:21:38 +0200 Subject: [PATCH 160/285] Merge.cs, BomEntity.cs: refactor BomEntityListMergeHelperStrategy for slow careful merges in BomEntityListMergeHelper class, to also dedup input list1 and to not skip work due to null/empty inputs Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 18 ++- src/CycloneDX.Core/Models/BomEntity.cs | 201 ++++++++++++++++--------- 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index ab0cd623..486d2ded 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -44,10 +44,16 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + // Rule out utterly empty inputs + if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) + { + if (list1 is not null) return list1; + if (list2 is not null) return list2; + return new List(); + } - if (typeof(BomEntity).IsInstanceOfType(list1[0])) + // At least one of these entries exists, per above sanity check + if (typeof(BomEntity).IsInstanceOfType((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0])) { MethodInfo methodMerge = null; Object helper; @@ -82,7 +88,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge for legacy types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"List-Merge for legacy types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + + if (list1 is null || list1.Count < 1) return list2; + if (list2 is null || list2.Count < 1) return list1; + var result = new List(list1); foreach (var item in list2) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a5374f4b..ebe55899 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -81,7 +81,6 @@ public class BomEntityListMergeHelperStrategy /// public bool useBomEntityMerge; - /// /// CycloneDX spec version. /// @@ -106,116 +105,182 @@ public class BomEntityListMergeHelper where T : BomEntity { public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { - //return BomUtils.MergeBomEntityLists(list1, list2); if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) iDebugLevel = 0; - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + // Rule out utterly empty inputs + if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) + { + if (list1 is not null) return list1; + if (list2 is not null) return list2; + return new List(); + } + + List result = new List(); + // Note: no blind checks for null/empty inputs - part of logic below, + // in order to surely de-duplicate even single incoming lists. if (!listMergeHelperStrategy.useBomEntityMerge) { // Most BomEntity classes are not individually IEquatable to avoid the // copy-paste coding overhead, however they inherit the Equals() and // GetHashCode() methods from their base class. if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); List hashList = new List(); - List resultQ = new List(); + List hashList2 = new List(); // Exclude possibly pre-existing identical entries first, then similarly // handle data from the second list. Here we have the "benefit" of lack // of real content merging, so already saved items (and their hashes) // can be treated as immutable. - foreach (T item1 in list1) + if (!(list1 is null) && list1.Count > 0) { - if (item1 is null) continue; - int hash1 = item1.GetHashCode(); - if (hashList.Contains(hash1)) - continue; - resultQ.Add(item1); - hashList.Add(hash1); + foreach (T item1 in list1) + { + if (item1 is null) continue; + int hash1 = item1.GetHashCode(); + if (hashList.Contains(hash1)) + { + if (iDebugLevel >= 1) + Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list1: ${item1.SerializeEntity()}"); + continue; + } + result.Add(item1); + hashList.Add(hash1); + } } - foreach (T item2 in list2) + if (!(list2 is null) && list2.Count > 0) { - if (item2 is null) continue; - int hash2 = item2.GetHashCode(); - if (hashList.Contains(hash2)) - continue; - resultQ.Add(item2); - hashList.Add(hash2); + foreach (T item2 in list2) + { + if (item2 is null) continue; + int hash2 = item2.GetHashCode(); + + // For info (track if data is bad or hash is unreliably weak): + if (iDebugLevel >= 1) + { + if (hashList2.Contains(hash2)) + { + Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list2: ${item2.SerializeEntity()}"); + } + hashList2.Add(hash2); + } + + if (hashList.Contains(hash2)) + continue; + result.Add(item2); + hashList.Add(hash2); + } } - return resultQ; + return result; } + // Here both lists are assumed to possibly have same or equivalent + // entries, even inside the same original list (e.g. if prepared by + // quick logic above for de-duplicating the major bulk of content). + Type TType = ((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0]).GetType(); + if (iDebugLevel >= 1) - Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {list1.GetType().ToString()}"); + Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {TType.ToString()}"); - List result = new List(list1); - Type TType = list1[0].GetType(); if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { methodMergeWith = null; } - foreach (var item2 in list2) + // Compact version of loop below; see comments there. + // In short, we avoid making a plain copy of list1 so + // we can carefully pass each entry to MergeWith() + // any suitable other in the same original list. + if (!(list1 is null) && list1.Count > 0) { - bool isContained = false; - if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + foreach (var item0 in list1) + { + for (int i=0; i < result.Count; i++) + { + var item1 = result[i]; + bool resMerge; + if (methodMergeWith != null) + { + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0}); + } + else + { + resMerge = item1.MergeWith(item0); + } - for (int i=0; i < result.Count; i++) + if (resMerge) + { + break; // item2 merged into result[item1] or already equal to it + } + } + } + } + + // Similar logic to the pass above, but with optional logging to + // highlight results of merges of the second list into the first. + if (!(list2 is null) && list2.Count > 0) + { + foreach (var item2 in list2) { + bool isContained = false; if (iDebugLevel >= 3) - Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); - var item1 = result[i]; - - // Squash contents of the new entry with an already - // existing equivalent (same-ness is subject to - // IEquatable<>.Equals() checks defined in respective - // classes), if there is a method defined there. - // For BomEntity descendant instances we assume that - // they have Equals(), Equivalent() and MergeWith() - // methods defined or inherited as is suitable for - // the particular entity type, hence much less code - // and error-checking than there was in the PoC: - bool resMerge; - if (methodMergeWith != null) + Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + + for (int i=0; i < result.Count; i++) { - resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + if (iDebugLevel >= 3) + Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + var item1 = result[i]; + + // Squash contents of the new entry with an already + // existing equivalent (same-ness is subject to + // IEquatable<>.Equals() checks defined in respective + // classes), if there is a method defined there. + // For BomEntity descendant instances we assume that + // they have Equals(), Equivalent() and MergeWith() + // methods defined or inherited as is suitable for + // the particular entity type, hence much less code + // and error-checking than there was in the PoC: + bool resMerge; + if (methodMergeWith != null) + { + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + } + else + { + resMerge = item1.MergeWith(item2); + } + // MergeWith() may throw BomEntityConflictException which we + // want to propagate to users - their input data is confusing. + // Probably should not throw BomEntityIncompatibleException + // unless the lists truly are of mixed types. + + if (resMerge) + { + isContained = true; + break; // item2 merged into result[item1] or already equal to it + } } - else + + if (isContained) { - resMerge = item1.MergeWith(item2); + if (iDebugLevel >= 2) + Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); } - // MergeWith() may throw BomEntityConflictException which we - // want to propagate to users - their input data is confusing. - // Probably should not throw BomEntityIncompatibleException - // unless the lists truly are of mixed types. - - if (resMerge) + else { - isContained = true; - break; // item2 merged into result[item1] or already equal to it + // Add new entry "as is" (new-ness is subject to + // equality checks of respective classes): + if (iDebugLevel >= 2) + Console.WriteLine($"WILL ADD: {item2.ToString()}"); + result.Add(item2); } } - - if (isContained) - { - if (iDebugLevel >= 2) - Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); - } - else - { - // Add new entry "as is" (new-ness is subject to - // equality checks of respective classes): - if (iDebugLevel >= 2) - Console.WriteLine($"WILL ADD: {item2.ToString()}"); - result.Add(item2); - } } return result; From 7c7aa82d89088aaedd05eb54ac610cb62ad61438 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 01:25:24 +0200 Subject: [PATCH 161/285] Merge.cs, BomEntity.cs et al: clean up commented-away experimental and obsoleted code before PR Signed-off-by: Jim Klimov --- .../Json/Serializer.Serialization.cs | 32 ------------------- src/CycloneDX.Core/Models/Component.cs | 25 +++------------ src/CycloneDX.Core/Models/Dependency.cs | 12 ------- src/CycloneDX.Core/Models/Service.cs | 11 ------- src/CycloneDX.Core/Models/Tool.cs | 13 -------- .../Models/Vulnerabilities/Vulnerability.cs | 12 ------- 6 files changed, 5 insertions(+), 100 deletions(-) diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index d7cc2fc1..cd862512 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -121,37 +121,5 @@ internal static string Serialize(BomEntity entity, JsonSerializerOptions jserOpt } return res; } - -/* - internal static string Serialize(Component component) - { - Contract.Requires(component != null); - return JsonSerializer.Serialize(component, _options); - } - - internal static string Serialize(Dependency dependency) - { - Contract.Requires(dependency != null); - return JsonSerializer.Serialize(dependency, _options); - } - - internal static string Serialize(Service service) - { - Contract.Requires(service != null); - return JsonSerializer.Serialize(service, _options); - } - - internal static string Serialize(Tool tool) - { - Contract.Requires(tool != null); - return JsonSerializer.Serialize(tool, _options); - } - - internal static string Serialize(Models.Vulnerabilities.Vulnerability vulnerability) - { - Contract.Requires(vulnerability != null); - return JsonSerializer.Serialize(vulnerability, _options); - } -*/ } } diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 96e394c8..74373c8d 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -201,18 +201,6 @@ public bool NonNullableModified public ReleaseNotes ReleaseNotes { get; set; } public bool ShouldSerializeReleaseNotes() { return ReleaseNotes != null; } -/* - public bool Equals(Component obj) - { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - } - - public override int GetHashCode() - { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - } -*/ - public bool Equivalent(Component obj) { return (!(obj is null) && this.BomRef == obj.BomRef); @@ -263,14 +251,14 @@ public bool MergeWith(Component obj) // merge the attribute values with help of reflection: if (iDebugLevel >= 1) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); - PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; //this.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; if (iDebugLevel >= 2) Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); // Use a temporary clone instead of mangling "this" object right away; // note serialization seems to skip over "nonnullable" values in some cases Component tmp = new Component(); - /* This fails due to copy of "non-null" fields which may be null: + /* This copier fails due to copy of "non-null" fields which may be null: * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); */ foreach (PropertyInfo property in properties) @@ -301,10 +289,7 @@ public bool MergeWith(Component obj) { case Type _ when property.PropertyType == typeof(Nullable): break; -/* - case Type _ when property.PropertyType == typeof(Nullable[]): - break; -*/ + case Type _ when property.PropertyType == typeof(ComponentScope): { // NOTE: Intentionally not matching 'Scope' helper @@ -591,7 +576,6 @@ public bool MergeWith(Component obj) default: { if ( - /* property.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute") || */ property.PropertyType.Name.StartsWith("Nullable") || property.PropertyType.ToString().StartsWith("System.Nullable") ) @@ -641,7 +625,8 @@ public bool MergeWith(Component obj) } } - + // Track the result of comparison, and if we did find and + // run a method for comparison (the result was "learned"): bool propsSeemEqual = false; bool propsSeemEqualLearned = false; diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 34b7dfb8..70a2e948 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -35,17 +35,5 @@ public class Dependency : BomEntity [XmlElement("dependency")] [ProtoMember(2)] public List Dependencies { get; set; } - -/* - public bool Equals(Dependency 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/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 38afffcf..9de775b3 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -123,16 +123,5 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } -/* - public bool Equals(Service 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.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 8a3d5cde..6674462c 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -46,18 +46,5 @@ public class Tool : BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } -/* - public bool Equals(Tool obj) - { - //return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - return base.Equals(obj); - } - - public override int GetHashCode() - { - //return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - return base.GetHashCode(); - } -*/ } } diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index c3daddeb..372e8247 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -124,17 +124,5 @@ public DateTime? Updated [ProtoMember(18)] public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } - -/* - public bool Equals(Vulnerability obj) - { - return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); - } - - public override int GetHashCode() - { - return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); - } -*/ } } From 2ee5906001823f626b09826a2aacde966c49ad77 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 02:42:06 +0200 Subject: [PATCH 162/285] Merge.cs, BomEntity.cs et al: address CI complaints (code style, etc.) ...and the bike-shed must be green! Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 28 +++- src/CycloneDX.Core/Models/BomEntity.cs | 176 +++++++++++++++++-------- src/CycloneDX.Core/Models/Component.cs | 122 ++++++++++++++--- src/CycloneDX.Core/Models/Hash.cs | 16 +-- src/CycloneDX.Utils/Merge.cs | 93 ++++++++++--- 5 files changed, 334 insertions(+), 101 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 486d2ded..553f6067 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -42,13 +42,21 @@ public List Merge(List list1, List list2) public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) return list1; - if (list2 is not null) return list2; + if (list1 is not null) + { + return list1; + } + if (list2 is not null) + { + return list2; + } return new List(); } @@ -71,7 +79,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat var constructedListHelperType = listHelperType.MakeGenericType(typeof(T)); helper = Activator.CreateInstance(constructedListHelperType); // Gotta use reflection for run-time evaluated type methods: - methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); + methodMerge = constructedListHelperType.GetMethod("Merge", 0, new [] { typeof(List), typeof(List), typeof(BomEntityListMergeHelperStrategy) }); } if (methodMerge != null) @@ -82,16 +90,26 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { // Should not get here, but if we do - log and fall through if (iDebugLevel >= 1) + { Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + } } } // Lists of legacy types (for BomEntity we use BomEntityListMergeHelper class) if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge for legacy types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + } - if (list1 is null || list1.Count < 1) return list2; - if (list2 is null || list2.Count < 1) return list1; + if (list1 is null || list1.Count < 1) + { + return list2; + } + if (list2 is null || list2.Count < 1) + { + return list1; + } var result = new List(list1); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ebe55899..bdc5f58b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -28,11 +28,11 @@ namespace CycloneDX.Models public class BomEntityConflictException : Exception { public BomEntityConflictException() - : base(String.Format("Unresolvable conflict in Bom entities")) + : base("Unresolvable conflict in Bom entities") { } public BomEntityConflictException(Type type) - : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type.ToString())) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}", type)) { } public BomEntityConflictException(string msg) @@ -40,7 +40,7 @@ public BomEntityConflictException(string msg) { } public BomEntityConflictException(string msg, Type type) - : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type.ToString(), msg)) + : base(String.Format("Unresolvable conflict in Bom entities of type {0}: {1}", type, msg)) { } } @@ -48,11 +48,11 @@ public BomEntityConflictException(string msg, Type type) public class BomEntityIncompatibleException : Exception { public BomEntityIncompatibleException() - : base(String.Format("Comparing incompatible Bom entities")) + : base("Comparing incompatible Bom entities") { } public BomEntityIncompatibleException(Type type1, Type type2) - : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1.ToString(), type2.ToString())) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}", type1, type2)) { } public BomEntityIncompatibleException(string msg) @@ -60,7 +60,7 @@ public BomEntityIncompatibleException(string msg) { } public BomEntityIncompatibleException(string msg, Type type1, Type type2) - : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1.ToString(), type2.ToString(), msg)) + : base(String.Format("Comparing incompatible Bom entities of types {0} and {1}: {2}", type1, type2, msg)) { } } @@ -93,7 +93,7 @@ public class BomEntityListMergeHelperStrategy /// which the callers can tune to their liking. public static BomEntityListMergeHelperStrategy Default() { - return new BomEntityListMergeHelperStrategy() + return new BomEntityListMergeHelperStrategy { useBomEntityMerge = true, specificationVersion = SpecificationVersionHelpers.CurrentVersion @@ -106,13 +106,21 @@ public class BomEntityListMergeHelper where T : BomEntity public List Merge(List list1, List list2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) return list1; - if (list2 is not null) return list2; + if (list1 is not null) + { + return list1; + } + if (list2 is not null) + { + return list2; + } return new List(); } @@ -126,7 +134,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // copy-paste coding overhead, however they inherit the Equals() and // GetHashCode() methods from their base class. if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge (quick and careless) for BomEntity-derived types: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); + } List hashList = new List(); List hashList2 = new List(); @@ -139,12 +149,17 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (T item1 in list1) { - if (item1 is null) continue; + if (item1 is null) + { + continue; + } int hash1 = item1.GetHashCode(); if (hashList.Contains(hash1)) { if (iDebugLevel >= 1) + { Console.WriteLine($"LIST-MERGE: hash table claims duplicate data in original list1: ${item1.SerializeEntity()}"); + } continue; } result.Add(item1); @@ -156,7 +171,10 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (T item2 in list2) { - if (item2 is null) continue; + if (item2 is null) + { + continue; + } int hash2 = item2.GetHashCode(); // For info (track if data is bad or hash is unreliably weak): @@ -170,7 +188,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } if (hashList.Contains(hash2)) + { continue; + } result.Add(item2); hashList.Add(hash2); } @@ -185,7 +205,9 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat Type TType = ((!(list1 is null) && list1.Count > 0) ? list1[0] : list2[0]).GetType(); if (iDebugLevel >= 1) + { Console.WriteLine($"List-Merge (careful) for BomEntity derivatives: {TType.ToString()}"); + } if (!BomEntity.KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { @@ -229,12 +251,16 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { bool isContained = false; if (iDebugLevel >= 3) + { Console.WriteLine($"result<{TType.ToString()}> now contains {result.Count} entries"); + } for (int i=0; i < result.Count; i++) { if (iDebugLevel >= 3) + { Console.WriteLine($"result<{TType.ToString()}>: checking entry #{i}"); + } var item1 = result[i]; // Squash contents of the new entry with an already @@ -270,14 +296,18 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat if (isContained) { if (iDebugLevel >= 2) + { Console.WriteLine($"ALREADY THERE: {item2.ToString()}"); + } } else { // Add new entry "as is" (new-ness is subject to // equality checks of respective classes): if (iDebugLevel >= 2) + { Console.WriteLine($"WILL ADD: {item2.ToString()}"); + } result.Add(item2); } } @@ -324,7 +354,7 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - public static List KnownEntityTypes = + public static readonly List KnownEntityTypes = new Func>(() => { List derived_types = new List(); @@ -343,7 +373,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownEntityTypeProperties = + public static readonly Dictionary KnownEntityTypeProperties = new Func>(() => { Dictionary dict = new Dictionary(); @@ -354,7 +384,7 @@ public class BomEntity : IEquatable return dict; }) (); - public static Dictionary KnownEntityTypeLists = + public static readonly Dictionary KnownEntityTypeLists = new Func>(() => { Dictionary dict = new Dictionary(); @@ -364,7 +394,7 @@ public class BomEntity : IEquatable // to craft a List "result" at run-time: Type listType = typeof(List<>); Type constructedListType = listType.MakeGenericType(type); - // Needed? var helper = Activator.CreateInstance(constructedListType); + // Would we want to stach a pre-created helper instance as Activator.CreateInstance(constructedListType) ? dict[type] = new BomEntityListReflection(); dict[type].genericType = constructedListType; @@ -372,8 +402,8 @@ public class BomEntity : IEquatable // Gotta use reflection for run-time evaluated type methods: dict[type].propCount = constructedListType.GetProperty("Count"); dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); - dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new Type[] { type }); - dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new Type[] { constructedListType }); + dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); + dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. @@ -382,7 +412,7 @@ public class BomEntity : IEquatable return dict; }) (); - public static Dictionary KnownBomEntityListMergeHelpers = + public static readonly Dictionary KnownBomEntityListMergeHelpers = new Func>(() => { Dictionary dict = new Dictionary(); @@ -402,14 +432,14 @@ public class BomEntity : IEquatable if (LType != null) { // Gotta use reflection for run-time evaluated type methods: - var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new Type[] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); + var methodMerge = constructedListHelperType.GetMethod("Merge", 0, new [] { LType, LType, typeof(BomEntityListMergeHelperStrategy) }); if (methodMerge != null) { dict[type] = new BomEntityListMergeHelperReflection(); dict[type].genericType = constructedListHelperType; dict[type].methodMerge = methodMerge; dict[type].helperInstance = helper; - // Callers would return (List)methodMerge.Invoke(helper, new object[] {list1, list2}); + // Callers would return something like (List)methodMerge.Invoke(helper, new object[] {list1, list2}) } else { @@ -430,21 +460,23 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - public static Dictionary KnownTypeSerializers = + public static readonly Dictionary KnownTypeSerializers = new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); var methodDefault = jserClassType.GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { var method = jserClassType.GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null && method != methodDefault) + { dict[type] = method; + } } return dict; }) (); @@ -454,7 +486,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeEquals = + public static readonly Dictionary KnownTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -462,27 +494,31 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); - public static Dictionary KnownDefaultEquals = + public static readonly Dictionary KnownDefaultEquals = new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method == null) + { dict[type] = methodDefault; + } } return dict; }) (); @@ -490,7 +526,7 @@ public class BomEntity : IEquatable // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. - public static Dictionary KnownOtherTypeEquals = + public static readonly Dictionary KnownOtherTypeEquals = new Func>(() => { Dictionary dict = new Dictionary(); @@ -502,7 +538,7 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); dict[type] = method; } return dict; @@ -513,7 +549,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeEquivalent = + public static readonly Dictionary KnownTypeEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); @@ -521,27 +557,31 @@ public class BomEntity : IEquatable { var method = type.GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); - public static Dictionary KnownDefaultEquivalent = + public static readonly Dictionary KnownDefaultEquivalent = new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { typeof(BomEntity) }); + new [] { typeof(BomEntity) }); foreach (var type in KnownEntityTypes) { var method = type.GetMethod("Equivalent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method == null) + { dict[type] = methodDefault; + } } return dict; }) (); @@ -551,7 +591,7 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - public static Dictionary KnownTypeMergeWith = + public static readonly Dictionary KnownTypeMergeWith = new Func>(() => { Dictionary dict = new Dictionary(); @@ -559,16 +599,18 @@ public class BomEntity : IEquatable { var method = type.GetMethod("MergeWith", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] { type }); + new [] { type }); if (method != null) + { dict[type] = method; + } } return dict; }) (); protected BomEntity() { - // a bad alternative to private is to: throw new NotImplementedException("The BomEntity class directly should not be instantiated"); + // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") } /// @@ -600,20 +642,33 @@ internal string SerializeEntity() /// should be by default aware and capable of ultimately serializing the properties /// relevant to each derived class. /// - /// Another BomEntity-derived object of same type + /// Another BomEntity-derived object of same type /// True if two objects are deemed equal - public bool Equals(BomEntity other) + public bool Equals(BomEntity obj) { Type thisType = this.GetType(); if (KnownTypeEquals.TryGetValue(thisType, out var methodEquals)) { - return (bool)methodEquals.Invoke(this, new object[] {other}); + return (bool)methodEquals.Invoke(this, new object[] {obj}); } - if (other is null || thisType != other.GetType()) return false; - return this.SerializeEntity() == other.SerializeEntity(); + if (obj is null || thisType != obj.GetType()) + { + return false; + } + return this.SerializeEntity() == obj.SerializeEntity(); } - + + // Needed by IEquatable contract + public override bool Equals(Object obj) + { + if (obj is null || !(obj is BomEntity)) + { + return false; + } + return this.Equals((BomEntity)obj); + } + public override int GetHashCode() { return this.SerializeEntity().GetHashCode(); @@ -626,23 +681,23 @@ public override int GetHashCode() /// are not) by defining an implementation tailored to that derived type /// as the argument, or keep this default where equiality is equivalence. /// - /// Another object of same type + /// Another object of same type /// True if two data objects are considered to represent /// the same real-life entity, False otherwise. - public bool Equivalent(BomEntity other) + public bool Equivalent(BomEntity obj) { Type thisType = this.GetType(); if (KnownTypeEquivalent.TryGetValue(thisType, out var methodEquivalent)) { - // Note we do not check for null/type of "other" at this point + // Note we do not check for null/type of "obj" at this point // since the derived classes define the logic of equivalence // (possibly to other entity subtypes as well). - return (bool)methodEquivalent.Invoke(this, new object[] {other}); + return (bool)methodEquivalent.Invoke(this, new object[] {obj}); } // Note that here a default Equivalent() may call into custom Equals(), // so the similar null/type sanity shecks are still relevant. - return (!(other is null) && (thisType == other.GetType()) && this.Equals(other)); + return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } /// @@ -653,7 +708,7 @@ public bool Equivalent(BomEntity other) /// Treats a null "other" object as a success (it is effectively a /// no-op merge, which keeps "this" object as is). /// - /// Another object of same type whose additional + /// Another object of same type whose additional /// non-conflicting data we try to squash into this object. /// True if merge was successful, False if it these objects /// are not equivalent, or throws if merge can not be done (including @@ -661,24 +716,33 @@ public bool Equivalent(BomEntity other) /// /// Source data problem: two entities with conflicting information /// Caller error: somehow merging different entity types - public bool MergeWith(BomEntity other) + public bool MergeWith(BomEntity obj) { - if (other is null) return true; - if (this.GetType() != other.GetType()) + if (obj is null) + { + return true; + } + if (this.GetType() != obj.GetType()) { // Note: potentially descendent classes can catch this // to adapt their behavior... if some two different // classes would ever describe something comparable // in real life. - throw new BomEntityIncompatibleException(this.GetType(), other.GetType()); + throw new BomEntityIncompatibleException(this.GetType(), obj.GetType()); } - if (this.Equals(other)) return true; + if (this.Equals(obj)) + { + return true; + } // Avoid calling Equals => serializer twice for no gain // (default equivalence is equality): if (KnownTypeEquivalent.TryGetValue(this.GetType(), out var methodEquivalent)) { - if (!this.Equivalent(other)) return false; + if (!this.Equivalent(obj)) + { + return false; + } // else fall through to exception below } else diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 74373c8d..55aecb09 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -119,7 +119,9 @@ public ComponentScope NonNullableScope get { if (Scope == null) + { return ComponentScope.Null; + } return Scope.Value; } set @@ -209,7 +211,9 @@ public bool Equivalent(Component obj) public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } try { @@ -226,7 +230,9 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related"); + } } } return resBase; @@ -247,19 +253,23 @@ public bool MergeWith(Component obj) (this.BomRef != null && this.BomRef.Equals(obj.BomRef)) || (this.Group == obj.Group && this.Name == obj.Name && this.Version == obj.Version) ) { - // Objects seem equivalent according to critical arguments; + // Objects seem equivalent according to critical arguments => // merge the attribute values with help of reflection: if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + } PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[this.GetType()]; if (iDebugLevel >= 2) + { Console.WriteLine($"Component.MergeWith(): items seem related - investigate properties: num {properties.Length}: {properties.ToString()}"); + } - // Use a temporary clone instead of mangling "this" object right away; - // note serialization seems to skip over "nonnullable" values in some cases + // Use a temporary clone instead of mangling "this" object right away. + // Note: serialization seems to skip over "nonnullable" values in some cases. Component tmp = new Component(); /* This copier fails due to copy of "non-null" fields which may be null: - * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)); + * tmp = JsonSerializer.Deserialize(CycloneDX.Json.Serializer.Serialize(this)) */ foreach (PropertyInfo property in properties) { @@ -267,11 +277,11 @@ public bool MergeWith(Component obj) // Avoid spurious "modified=false" in merged JSON // Also skip helpers, care about real values if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(this.Modified.HasValue)) { - // Can not set R/O prop: ### tmp.Modified.HasValue = false; + // Can not set R/O prop: ### tmp.Modified.HasValue = false continue; } if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(this.Scope.HasValue)) { - // Can not set R/O prop: ### tmp.Scope.HasValue = false; + // Can not set R/O prop: ### tmp.Scope.HasValue = false continue; } property.SetValue(tmp, property.GetValue(this, null)); @@ -284,7 +294,9 @@ public bool MergeWith(Component obj) foreach (PropertyInfo property in properties) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): <{property.PropertyType}>'{property.Name}'"); + } switch (property.PropertyType) { case Type _ when property.PropertyType == typeof(Nullable): @@ -295,7 +307,9 @@ public bool MergeWith(Component obj) // NOTE: Intentionally not matching 'Scope' helper // Not nullable! Quickly keep un-set if applicable. if (!(obj.Scope.HasValue) && !(tmp.Scope.HasValue)) + { continue; + } ComponentScope tmpItem; try @@ -328,7 +342,9 @@ public bool MergeWith(Component obj) } if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: '{tmpItem}' and '{objItem}'"); + } // Since CycloneDX spec v1.0 up to at least v1.4, // an absent value "SHOULD" be treated as "required" @@ -338,7 +354,9 @@ public bool MergeWith(Component obj) if (tmpItem == ComponentScope.Null && objItem == ComponentScope.Null) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: keep unspecified explicitly"); + } continue; } @@ -346,7 +364,9 @@ public bool MergeWith(Component obj) { property.SetValue(tmp, ComponentScope.Optional); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: keep 'Optional'"); + } continue; } @@ -354,7 +374,9 @@ public bool MergeWith(Component obj) // keep absent=>required; upgrade optional objItem property.SetValue(tmp, ComponentScope.Required); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Required'"); + } continue; } @@ -369,7 +391,9 @@ public bool MergeWith(Component obj) // downgrade optional objItem to excluded property.SetValue(tmp, ComponentScope.Excluded); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): SCOPE: set 'Excluded'"); + } continue; } @@ -379,7 +403,9 @@ public bool MergeWith(Component obj) // avoid conflicts; be sure then to check for other entries that have // everything same except bom-ref (match the expected new pattern)?.. if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): WARNING: can not merge two bom-refs with scope excluded and required"); + } mergedOk = false; } break; @@ -388,14 +414,22 @@ public bool MergeWith(Component obj) { // Not nullable! Keep un-set if applicable. if (!obj.Modified.HasValue) + { continue; + } + bool tmpItem = (bool)property.GetValue(tmp, null); bool objItem = (bool)property.GetValue(obj, null); if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): MODIFIED BOOL: '{tmpItem}' and '{objItem}'"); + } + if (objItem) + { property.SetValue(tmp, true); + } } break; @@ -407,7 +441,9 @@ public bool MergeWith(Component obj) if (propValTmp == null && propValObj == null) { if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): LIST?: got in tmp and in obj"); + } continue; } @@ -425,7 +461,9 @@ public bool MergeWith(Component obj) else { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): No cached info about BomEntityListReflection[{LType}]"); + } propCount = LType.GetProperty("Count"); methodGetItem = LType.GetMethod("get_Item"); methodAdd = LType.GetMethod("Add"); @@ -434,7 +472,9 @@ public bool MergeWith(Component obj) if (methodGetItem == null || propCount == null || methodAdd == null) { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): WARNING: is this really a LIST - it lacks a get_Item() or Add() method, or a Count property"); + } mergedOk = false; continue; } @@ -442,7 +482,9 @@ public bool MergeWith(Component obj) int propValTmpCount = (propValTmp == null ? -1 : (int)propCount.GetValue(propValTmp, null)); int propValObjCount = (propValObj == null ? -1 : (int)propCount.GetValue(propValObj, null)); if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): LIST?: got {propValTmp}=>{propValTmpCount} in tmp and {propValObj}=>{propValObjCount} in obj"); + } if (propValObj == null || propValObjCount < 1) { @@ -459,7 +501,7 @@ public bool MergeWith(Component obj) if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) methodMergeWith = null; } @@ -477,9 +519,11 @@ public bool MergeWith(Component obj) } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + methodEquals = TType.GetMethod("Equals", 0, new [] { TType }); if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } } @@ -488,7 +532,9 @@ public bool MergeWith(Component obj) { var objItem = methodGetItem.Invoke(propValObj, new object[] { o }); if (objItem is null) + { continue; + } bool listHit = false; for (int t = 0; t < propValTmpCount && !listHit; t++) @@ -505,8 +551,10 @@ public bool MergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): try methodEquals()"); - propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new object[] {objItem}); + } + propsSeemEqual = (bool)methodEquals.Invoke(tmpItem, new [] {objItem}); propsSeemEqualLearned = true; } } @@ -514,7 +562,9 @@ public bool MergeWith(Component obj) { // no-op if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + } } if (propsSeemEqual || !propsSeemEqualLearned) @@ -528,21 +578,29 @@ public bool MergeWith(Component obj) try { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); - if (!((bool)methodMergeWith.Invoke(tmpItem, new object[] {objItem}))) + } + if (!((bool)methodMergeWith.Invoke(tmpItem, new [] {objItem}))) + { mergedOk = false; + } } catch (System.Exception exc) { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: {exc.ToString()}"); + } mergedOk = false; } } // else: no method, just trust equality - avoid "Add" to merge below else { if (iDebugLevel >= 7) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmpItem?.ToString()} and {objItem?.ToString()}: no such method: will add to list"); + } } } // else: tmpitem considered not equal, should be added } @@ -550,7 +608,7 @@ public bool MergeWith(Component obj) if (!listHit) { - methodAdd.Invoke(propValTmp, new object[] {objItem}); + methodAdd.Invoke(propValTmp, new [] {objItem}); propValTmpCount = (int)propCount.GetValue(propValTmp, null); } } @@ -584,12 +642,16 @@ public bool MergeWith(Component obj) // followed by '{ComponentScope NonNullableScope}' // which we specially handle above if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP NullableAttribute"); + } continue; } if (iDebugLevel >= 4) + { Console.WriteLine($"Component.MergeWith(): DEFAULT TYPES"); + } var propValTmp = property.GetValue(tmp, null); var propValObj = property.GetValue(obj, null); if (propValObj == null) @@ -618,9 +680,11 @@ public bool MergeWith(Component obj) } else { - methodEquals = TType.GetMethod("Equals", 0, new Type[] { TType }); + methodEquals = TType.GetMethod("Equals", 0, new [] { TType }); if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): had to look for {TType}.Equals()... found? {methodEquals != null}"); + } } } } @@ -635,8 +699,10 @@ public bool MergeWith(Component obj) if (methodEquals != null) { if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): try methodEquals()"); - propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new object[] {propValObj}); + } + propsSeemEqual = (bool)methodEquals.Invoke(propValTmp, new [] {propValObj}); propsSeemEqualLearned = true; } } @@ -644,7 +710,9 @@ public bool MergeWith(Component obj) { // no-op if (iDebugLevel >= 6) + { Console.WriteLine($"Component.MergeWith(): can not check Equals() {propValTmp.ToString()} and {propValObj.ToString()}: {exc.ToString()}"); + } } try @@ -653,7 +721,9 @@ public bool MergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): MIGHT SKIP MERGE: items say they are equal"); + } propsSeemEqual = propValTmp.Equals(propValObj); propsSeemEqualLearned = true; } @@ -669,7 +739,9 @@ public bool MergeWith(Component obj) { // Fall back to generic equality check which may be useless if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: items say they are equal"); + } propsSeemEqual = (propValTmp == propValObj); propsSeemEqualLearned = true; } @@ -682,13 +754,15 @@ public bool MergeWith(Component obj) if (!propsSeemEqual) { if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): items say they are not equal"); + } } if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new Type[] { TType }); + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) methodMergeWith = null; } @@ -696,16 +770,22 @@ public bool MergeWith(Component obj) { try { - if (!((bool)methodMergeWith.Invoke(propValTmp, new object[] {propValObj}))) + if (!((bool)methodMergeWith.Invoke(propValTmp, new [] {propValObj}))) + { mergedOk = false; + } } catch (System.Exception exc) { // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) + { continue; + } if (iDebugLevel >= 5) + { Console.WriteLine($"Component.MergeWith(): FAILED MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: {exc.ToString()}"); + } mergedOk = false; } } @@ -713,9 +793,13 @@ public bool MergeWith(Component obj) { // That property's class lacks a mergeWith(), gotta trust the equality: if (propsSeemEqual) + { continue; + } if (iDebugLevel >= 7) + { Console.WriteLine($"Component.MergeWith(): SKIP MERGE: can not {this.GetType().ToString()}.mergeWith() '{property.Name}' of {tmp.ToString()} and {obj.ToString()}: no such method"); + } mergedOk = false; } } @@ -730,11 +814,11 @@ public bool MergeWith(Component obj) // Avoid spurious "modified=false" in merged JSON // Also skip helpers, care about real values if ((property.Name == "Modified" || property.Name == "NonNullableModified") && !(tmp.Modified.HasValue)) { - // Can not set R/O prop: ### this.Modified.HasValue = false; + // Can not set R/O prop: ### this.Modified.HasValue = false continue; } if ((property.Name == "Scope" || property.Name == "NonNullableScope") && !(tmp.Scope.HasValue)) { - // Can not set R/O prop: ### this.Scope.HasValue = false; + // Can not set R/O prop: ### this.Scope.HasValue = false continue; } property.SetValue(this, property.GetValue(tmp, null)); @@ -742,13 +826,17 @@ public bool MergeWith(Component obj) } if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): result {mergedOk} for: {this.BomRef} / {this.Group} : {this.Name} : {this.Version}"); + } return mergedOk; } else { if (iDebugLevel >= 1) + { Console.WriteLine($"Component.MergeWith(): SKIP: items do not seem related upon second look"); + } } // Merge was not applicable or otherwise did not succeed diff --git a/src/CycloneDX.Core/Models/Hash.cs b/src/CycloneDX.Core/Models/Hash.cs index 4f54fa6f..1bf4d761 100644 --- a/src/CycloneDX.Core/Models/Hash.cs +++ b/src/CycloneDX.Core/Models/Hash.cs @@ -63,32 +63,32 @@ public enum HashAlgorithm [ProtoMember(2)] public string Content { get; set; } - public bool Equivalent(Hash other) + public bool Equivalent(Hash obj) { - return (!(other is null) && this.Alg == other.Alg); + return (!(obj is null) && this.Alg == obj.Alg); } - public bool MergeWith(Hash other) + public bool MergeWith(Hash obj) { try { // Basic checks for null, type compatibility, // equality and non-equivalence; throws for // the hard stuff to implement in the catch: - return base.MergeWith(other); + return base.MergeWith(obj); } catch (BomEntityConflictException) { // Note: Alg is non-nullable so no check for that - if (this.Content is null && !(other.Content is null)) + if (this.Content is null && !(obj.Content is null)) { - this.Content = other.Content; + this.Content = obj.Content; return true; } - if (this.Content != other.Content) + if (this.Content != obj.Content) { - throw new BomEntityConflictException("Two Hash objects with same Alg='${this.Alg}' and different Content: '${this.Content}' vs. '${other.Content}'"); + throw new BomEntityConflictException($"Two Hash objects with same Alg='{this.Alg}' and different Content: '{this.Content}' vs. '{obj.Content}'"); } // All known properties merged or were equal/equivalent diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index c35008d6..fac1387d 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -58,7 +58,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } var result = new Bom(); result.Metadata = new Metadata @@ -95,21 +97,27 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy // twice should be effectively no-op); try to merge instead: if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); + } if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) { // bom1's entry is not null and seems equivalent to bom2's: - if (iDebugLevel >= 1) + if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); + } result.Metadata.Component = bom1.Metadata.Component; result.Metadata.Component.MergeWith(bom2.Metadata.Component); } else { - if (iDebugLevel >= 1) + if (iDebugLevel >= 1) + { Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); + } result.Components.Add(bom2.Metadata.Component); } } @@ -256,7 +264,10 @@ public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) if (bomSubject != null) { - if (bomSubject.BomRef is null) bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); + if (bomSubject.BomRef is null) + { + bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); + } result.Metadata.Component = bomSubject; result.Metadata.Tools = new List(); } @@ -365,23 +376,38 @@ bom.SerialNumber is null public static Bom CleanupMetadataComponent(Bom result) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { iDebugLevel = 0; + } if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); + } + if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) { if (iDebugLevel >= 2) + { Console.WriteLine($"MERGE-CLEANUP: Searching in list"); + } foreach (Component component in result.Components) { if (iDebugLevel >= 2) + { Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); - if (component is null) continue; // should not happen + } + if (component is null) + { + // should not happen, but... + continue; + } if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) { if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); + } result.Metadata.Component.MergeWith(component); result.Components.Remove(component); return result; @@ -390,20 +416,49 @@ public static Bom CleanupMetadataComponent(Bom result) } if (iDebugLevel >= 1) + { Console.WriteLine($"MERGE-CLEANUP: NO HITS"); + } return result; } public static Bom CleanupEmptyLists(Bom result) { // cleanup empty top level elements - if (result.Metadata?.Tools?.Count == 0) result.Metadata.Tools = null; - if (result.Components?.Count == 0) result.Components = null; - if (result.Services?.Count == 0) result.Services = null; - if (result.ExternalReferences?.Count == 0) result.ExternalReferences = null; - if (result.Dependencies?.Count == 0) result.Dependencies = null; - if (result.Compositions?.Count == 0) result.Compositions = null; - if (result.Vulnerabilities?.Count == 0) result.Vulnerabilities = null; + if (result.Metadata?.Tools?.Count == 0) + { + result.Metadata.Tools = null; + } + + if (result.Components?.Count == 0) + { + result.Components = null; + } + + if (result.Services?.Count == 0) + { + result.Services = null; + } + + if (result.ExternalReferences?.Count == 0) + { + result.ExternalReferences = null; + } + + if (result.Dependencies?.Count == 0) + { + result.Dependencies = null; + } + + if (result.Compositions?.Count == 0) + { + result.Compositions = null; + } + + if (result.Vulnerabilities?.Count == 0) + { + result.Vulnerabilities = null; + } return result; } @@ -435,9 +490,11 @@ private static void NamespaceComponentBomRefs(Component topComponent) var currentComponent = components.Pop(); if (currentComponent.Components != null) - foreach (var subComponent in currentComponent.Components) { - components.Push(subComponent); + foreach (var subComponent in currentComponent.Components) + { + components.Push(subComponent); + } } currentComponent.BomRef = NamespacedBomRef(topComponent, currentComponent.BomRef); @@ -473,9 +530,11 @@ private static void NamespaceDependencyBomRefs(string bomRefNamespace, List Date: Fri, 11 Aug 2023 03:46:11 +0200 Subject: [PATCH 163/285] BomEntity.cs: address CI complaints (protect prepared public lists and dicts as immutable) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 67 +++++++++++++------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index bdc5f58b..25f45aed 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -354,8 +355,8 @@ public class BomEntity : IEquatable /// /// List of classes derived from BomEntity, prepared startically at start time. /// - public static readonly List KnownEntityTypes = - new Func>(() => + public static readonly ImmutableList KnownEntityTypes = + new Func>(() => { List derived_types = new List(); foreach (var domain_assembly in AppDomain.CurrentDomain.GetAssemblies()) @@ -365,7 +366,7 @@ public class BomEntity : IEquatable derived_types.AddRange(assembly_types); } - return derived_types; + return ImmutableList.Create(derived_types.ToArray()); }) (); /// @@ -373,19 +374,19 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownEntityTypeProperties = - new Func>(() => + public static readonly ImmutableDictionary KnownEntityTypeProperties = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { dict[type] = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownEntityTypeLists = - new Func>(() => + public static readonly ImmutableDictionary KnownEntityTypeLists = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -409,11 +410,11 @@ public class BomEntity : IEquatable // TODO: Separate dict?.. dict[constructedListType] = dict[type]; } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownBomEntityListMergeHelpers = - new Func>(() => + public static readonly ImmutableDictionary KnownBomEntityListMergeHelpers = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -452,7 +453,7 @@ public class BomEntity : IEquatable throw new InvalidOperationException($"BomEntityListMergeHelper<{type}> lacks a List class definition"); } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -460,8 +461,8 @@ public class BomEntity : IEquatable /// MethodInfo about custom CycloneDX.Json.Serializer.Serialize() /// implementations (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeSerializers = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeSerializers = + new Func>(() => { var jserClassType = typeof(CycloneDX.Json.Serializer); var methodDefault = jserClassType.GetMethod("Serialize", @@ -478,7 +479,7 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -486,8 +487,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equals() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeEquals = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -500,11 +501,11 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownDefaultEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownDefaultEquals = + new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equals", @@ -520,14 +521,14 @@ public class BomEntity : IEquatable dict[type] = methodDefault; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); // Our loops check for some non-BomEntity typed value equalities, // so cache their methods if present. Note that this one retains // the "null" results to mark that we do not need to look further. - public static readonly Dictionary KnownOtherTypeEquals = - new Func>(() => + public static readonly ImmutableDictionary KnownOtherTypeEquals = + new Func>(() => { Dictionary dict = new Dictionary(); var listMore = new List(); @@ -541,7 +542,7 @@ public class BomEntity : IEquatable new [] { type }); dict[type] = method; } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -549,8 +550,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom Equivalent() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeEquivalent = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeEquivalent = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -563,11 +564,11 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); - public static readonly Dictionary KnownDefaultEquivalent = - new Func>(() => + public static readonly ImmutableDictionary KnownDefaultEquivalent = + new Func>(() => { Dictionary dict = new Dictionary(); var methodDefault = typeof(BomEntity).GetMethod("Equivalent", @@ -583,7 +584,7 @@ public class BomEntity : IEquatable dict[type] = methodDefault; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); /// @@ -591,8 +592,8 @@ public class BomEntity : IEquatable /// MethodInfo about their custom MergeWith() method implementations /// (if present), prepared startically at start time. /// - public static readonly Dictionary KnownTypeMergeWith = - new Func>(() => + public static readonly ImmutableDictionary KnownTypeMergeWith = + new Func>(() => { Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) @@ -605,7 +606,7 @@ public class BomEntity : IEquatable dict[type] = method; } } - return dict; + return ImmutableDictionary.CreateRange(dict); }) (); protected BomEntity() From 7f3837e1b9e2dcde4381ca6697b5e1992688bfed Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 03:46:35 +0200 Subject: [PATCH 164/285] BomEntity.cs: address CI complaints (protect helper class public fields with getters/setters) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 25f45aed..454da3e4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -80,12 +80,12 @@ public class BomEntityListMergeHelperStrategy /// deduplicating based on that (goes faster but /// may cause data structure not conforming to spec) /// - public bool useBomEntityMerge; + public bool useBomEntityMerge { get; set; } /// /// CycloneDX spec version. /// - public SpecificationVersion specificationVersion; + public SpecificationVersion specificationVersion { get; set; } /// /// Return reasonable default strategy settings. @@ -320,18 +320,18 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat public class BomEntityListReflection { - public Type genericType; - public PropertyInfo propCount; - public MethodInfo methodAdd; - public MethodInfo methodAddRange; - public MethodInfo methodGetItem; + public Type genericType { get; set; } + public PropertyInfo propCount { get; set; } + public MethodInfo methodAdd { get; set; } + public MethodInfo methodAddRange { get; set; } + public MethodInfo methodGetItem { get; set; } } public class BomEntityListMergeHelperReflection { - public Type genericType; - public MethodInfo methodMerge; - public Object helperInstance; + public Type genericType { get; set; } + public MethodInfo methodMerge { get; set; } + public Object helperInstance { get; set; } } /// From d54affe87f6bbe848204f6902b31262566fdefb9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 11 Aug 2023 09:11:19 +0200 Subject: [PATCH 165/285] ListMergeHelper: update doc summary of the class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 553f6067..c110aa9b 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -27,9 +27,14 @@ namespace CycloneDX /// Allows to merge generic lists with items of specified types /// (by default essentially adding entries which are not present /// yet according to List.Contains() method), and calls special - /// logic for lists of BomEntry types. + /// logic for lists of BomEntry types.
+ /// /// Used in CycloneDX.Utils various Merge implementations as well - /// as in CycloneDX.Core BomEntity-derived classes' MergeWith(). + /// as in CycloneDX.Core BomEntity-derived classes' MergeWith().
+ /// + /// Does not modify original lists and returns a new instance + /// with merged data. One exception is if one of the inputs is + /// null or empty - then the other object is returned. ///
/// Type of listed entries public class ListMergeHelper From 1f4c12b843e1b01d1054fb8b4decb6d48c843b04 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 14 Aug 2023 21:39:24 +0200 Subject: [PATCH 166/285] BomEntity.cs, ListMergeHelper.cs: avoid syntax that requires newer C# Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 4 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index c110aa9b..3ff73183 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -54,11 +54,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) + if (!(list1 is null)) { return list1; } - if (list2 is not null) + if (!(list2 is null)) { return list2; } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 454da3e4..cd956a5e 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -114,11 +114,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) + if (!(list1 is null)) { return list1; } - if (list2 is not null) + if (!(list2 is null)) { return list2; } From 5e2f78e0b773a1d7b07f1e7a3aab9be173515eff Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 14 Aug 2023 21:39:24 +0200 Subject: [PATCH 167/285] BomEntity.cs, ListMergeHelper.cs: avoid syntax that requires newer C# Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 4 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index c110aa9b..3ff73183 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -54,11 +54,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) + if (!(list1 is null)) { return list1; } - if (list2 is not null) + if (!(list2 is null)) { return list2; } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 454da3e4..cd956a5e 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -114,11 +114,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Rule out utterly empty inputs if ((list1 is null || list1.Count < 1) && (list2 is null || list2.Count < 1)) { - if (list1 is not null) + if (!(list1 is null)) { return list1; } - if (list2 is not null) + if (!(list2 is null)) { return list2; } From 44ab3a8d30918e56ac41705e5b10efa0eeedff9e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 16 Aug 2023 14:27:42 +0200 Subject: [PATCH 168/285] ListMergeHelper.cs: avoid potential null dereference in logging Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 3ff73183..539af552 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -96,7 +96,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Should not get here, but if we do - log and fall through if (iDebugLevel >= 1) { - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); } } } From f8807ced956e53212d21d47d7e80630e04facb7a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 16 Aug 2023 14:27:42 +0200 Subject: [PATCH 169/285] ListMergeHelper.cs: avoid potential null dereference in logging Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 3ff73183..539af552 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -96,7 +96,7 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Should not get here, but if we do - log and fall through if (iDebugLevel >= 1) { - Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1.GetType().ToString()} and {list2.GetType().ToString()}"); + Console.WriteLine($"Warning: List-Merge for BomEntity failed to find a Merge() helper method: {list1?.GetType()?.ToString()} and {list2?.GetType()?.ToString()}"); } } } From 5620e17334231d0cd0c94be96889e897aa0e9138 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 15:19:40 +0200 Subject: [PATCH 170/285] Merge.cs: update comments Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index fac1387d..ff25aea4 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -197,6 +197,9 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. + // TODO: Add reference to this build of cyclonedx-cli to the + // metadata/tools of the merged BOM document. After all - any bugs + // due to merge routines are our own... if (bomSubject is null) { var emptyBom = new Bom(); @@ -422,9 +425,13 @@ public static Bom CleanupMetadataComponent(Bom result) return result; } + /// + /// Clean up empty top level elements. + /// + /// A Bom document + /// Resulting document (whether modified or not) public static Bom CleanupEmptyLists(Bom result) { - // cleanup empty top level elements if (result.Metadata?.Tools?.Count == 0) { result.Metadata.Tools = null; From 5406535af968df34d7848b6b0977c6e36b0ea566 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 15:21:11 +0200 Subject: [PATCH 171/285] Merge.cs: introduce CleanupSortLists() as last step, and ListMergeHelper::SortByAscending()/SortByDescending() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 19 +++++++++ src/CycloneDX.Utils/Merge.cs | 55 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 539af552..268511f5 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -128,5 +128,24 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat return result; } + + // Adapted from https://stackoverflow.com/a/76523292/4715872 + public void SortByAscending(List list, Func selector, IComparer comparer = null) + { + if (comparer is null) + { + comparer = Comparer.Default; + } + list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + } + + public void SortByDescending(List list, Func selector, IComparer comparer = null) + { + if (comparer is null) + { + comparer = Comparer.Default; + } + list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index ff25aea4..fc58b285 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -239,6 +239,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result = CleanupMetadataComponent(result); result = CleanupEmptyLists(result); + result = CleanupSortLists(result); return result; } @@ -470,6 +471,60 @@ public static Bom CleanupEmptyLists(Bom result) return result; } + /// + /// Sort (top-level) list entries in the Bom for easier comparisons + /// and better compression.
+ /// TODO? Drill into the BomEntities to sort lists inside too? + ///
+ /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupSortLists(Bom result) + { + if (result.Metadata?.Tools?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Metadata.Tools, o => (o?.Vendor, o?.Name, o?.Version)); + } + + if (result.Components?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Components, o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version)); + } + + if (result.Services?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version)); + } + + if (result.ExternalReferences?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.ExternalReferences, o => (o?.Url, o?.Type)); + } + + if (result.Dependencies?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Dependencies, o => (o?.Ref)); + } + + if (result.Compositions?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Compositions, o => (o?.Aggregate, o?.Assemblies, o?.Dependencies)); + } + + if (result.Vulnerabilities?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Vulnerabilities, o => (o?.BomRef, o?.Id, o?.Created, o?.Updated)); + } + + return result; + } + private static string NamespacedBomRef(Component bomSubject, string bomRef) { return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); From c17ac40239f5d1d8f1379b9c132db2d4853f1330 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 16:32:36 +0200 Subject: [PATCH 172/285] BomEntity: PoC CompareSelector() offload into classes Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cd956a5e..27169edd 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -609,6 +609,48 @@ public class BomEntity : IEquatable return ImmutableDictionary.CreateRange(dict); }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom CompareSelector() method implementations + /// (if present), prepared startically at start time. + /// + public static readonly ImmutableDictionary KnownTypeCompareSelector = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("CompareSelector", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] {}); + if (method != null) + { + dict[type] = method; + } + } + return ImmutableDictionary.CreateRange(dict); + }) (); + + public static readonly ImmutableDictionary KnownDefaultCompareSelector = + new Func>(() => + { + Dictionary dict = new Dictionary(); + var methodDefault = typeof(BomEntity).GetMethod("CompareSelector", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] {}); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("CompareSelector", + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + new Type[] {}); + if (method == null) + { + dict[type] = methodDefault; + } + } + return ImmutableDictionary.CreateRange(dict); + }) (); + protected BomEntity() { // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") @@ -701,6 +743,25 @@ public bool Equivalent(BomEntity obj) return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } + /// + /// The "selector" in this expression: + ///
list.Sort((a, b) => comparer.Compare(selector(a), selector(b)));
+ /// and the "o => ..." part in + ///
sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version));
+ ///
+ /// Func object with some amount of arguments + public Func CompareSelector() + { + Type thisType = this.GetType(); + if (KnownTypeCompareSelector.TryGetValue(thisType, out var methodCompareSelector)) + { + return (Func)methodCompareSelector.Invoke(this, null); + } + + // Expensive but reliable; classes are welcome to implement theirs + return new Func((obj) => (ValueTuple)ValueTuple.Create(obj.SerializeEntity())); + } + /// /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false From 4e6fea60500819a336b9a0ea7e5a853bed305d37 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 16:32:48 +0200 Subject: [PATCH 173/285] Revert "BomEntity: PoC CompareSelector() offload into classes" This reverts commit c17ac40239f5d1d8f1379b9c132db2d4853f1330. Too much hassle to cast Tuple --- src/CycloneDX.Core/Models/BomEntity.cs | 61 -------------------------- 1 file changed, 61 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 27169edd..cd956a5e 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -609,48 +609,6 @@ public class BomEntity : IEquatable return ImmutableDictionary.CreateRange(dict); }) (); - /// - /// Dictionary mapping classes derived from BomEntity to reflection - /// MethodInfo about their custom CompareSelector() method implementations - /// (if present), prepared startically at start time. - /// - public static readonly ImmutableDictionary KnownTypeCompareSelector = - new Func>(() => - { - Dictionary dict = new Dictionary(); - foreach (var type in KnownEntityTypes) - { - var method = type.GetMethod("CompareSelector", - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] {}); - if (method != null) - { - dict[type] = method; - } - } - return ImmutableDictionary.CreateRange(dict); - }) (); - - public static readonly ImmutableDictionary KnownDefaultCompareSelector = - new Func>(() => - { - Dictionary dict = new Dictionary(); - var methodDefault = typeof(BomEntity).GetMethod("CompareSelector", - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] {}); - foreach (var type in KnownEntityTypes) - { - var method = type.GetMethod("CompareSelector", - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new Type[] {}); - if (method == null) - { - dict[type] = methodDefault; - } - } - return ImmutableDictionary.CreateRange(dict); - }) (); - protected BomEntity() { // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") @@ -743,25 +701,6 @@ public bool Equivalent(BomEntity obj) return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } - /// - /// The "selector" in this expression: - ///
list.Sort((a, b) => comparer.Compare(selector(a), selector(b)));
- /// and the "o => ..." part in - ///
sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version));
- ///
- /// Func object with some amount of arguments - public Func CompareSelector() - { - Type thisType = this.GetType(); - if (KnownTypeCompareSelector.TryGetValue(thisType, out var methodCompareSelector)) - { - return (Func)methodCompareSelector.Invoke(this, null); - } - - // Expensive but reliable; classes are welcome to implement theirs - return new Func((obj) => (ValueTuple)ValueTuple.Create(obj.SerializeEntity())); - } - /// /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false From 880c4167b55e880789938b21e438f3013ba91d69 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 15:19:40 +0200 Subject: [PATCH 174/285] Merge.cs: update comments Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index fac1387d..ff25aea4 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -197,6 +197,9 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. + // TODO: Add reference to this build of cyclonedx-cli to the + // metadata/tools of the merged BOM document. After all - any bugs + // due to merge routines are our own... if (bomSubject is null) { var emptyBom = new Bom(); @@ -422,9 +425,13 @@ public static Bom CleanupMetadataComponent(Bom result) return result; } + /// + /// Clean up empty top level elements. + /// + /// A Bom document + /// Resulting document (whether modified or not) public static Bom CleanupEmptyLists(Bom result) { - // cleanup empty top level elements if (result.Metadata?.Tools?.Count == 0) { result.Metadata.Tools = null; From 693223232ea1d5b540946ea34e6557c990c4d736 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 15:21:11 +0200 Subject: [PATCH 175/285] Merge.cs: introduce CleanupSortLists() as last step, and ListMergeHelper::SortByAscending()/SortByDescending() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 19 +++++++++ src/CycloneDX.Utils/Merge.cs | 55 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 539af552..268511f5 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -128,5 +128,24 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat return result; } + + // Adapted from https://stackoverflow.com/a/76523292/4715872 + public void SortByAscending(List list, Func selector, IComparer comparer = null) + { + if (comparer is null) + { + comparer = Comparer.Default; + } + list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + } + + public void SortByDescending(List list, Func selector, IComparer comparer = null) + { + if (comparer is null) + { + comparer = Comparer.Default; + } + list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index ff25aea4..fc58b285 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -239,6 +239,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result = CleanupMetadataComponent(result); result = CleanupEmptyLists(result); + result = CleanupSortLists(result); return result; } @@ -470,6 +471,60 @@ public static Bom CleanupEmptyLists(Bom result) return result; } + /// + /// Sort (top-level) list entries in the Bom for easier comparisons + /// and better compression.
+ /// TODO? Drill into the BomEntities to sort lists inside too? + ///
+ /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupSortLists(Bom result) + { + if (result.Metadata?.Tools?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Metadata.Tools, o => (o?.Vendor, o?.Name, o?.Version)); + } + + if (result.Components?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Components, o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version)); + } + + if (result.Services?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version)); + } + + if (result.ExternalReferences?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.ExternalReferences, o => (o?.Url, o?.Type)); + } + + if (result.Dependencies?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Dependencies, o => (o?.Ref)); + } + + if (result.Compositions?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Compositions, o => (o?.Aggregate, o?.Assemblies, o?.Dependencies)); + } + + if (result.Vulnerabilities?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Vulnerabilities, o => (o?.BomRef, o?.Id, o?.Created, o?.Updated)); + } + + return result; + } + private static string NamespacedBomRef(Component bomSubject, string bomRef) { return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); From ef320e965767a6d1a00337025b34f4dd548e0fd5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 16:47:04 +0200 Subject: [PATCH 176/285] Merge.cs: inject this cyclonedx-cli script into metadata/tools[] Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index fc58b285..0849769f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using CycloneDX; using CycloneDX.Models; using CycloneDX.Models.Vulnerabilities; @@ -197,21 +198,54 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. - // TODO: Add reference to this build of cyclonedx-cli to the - // metadata/tools of the merged BOM document. After all - any bugs - // due to merge routines are our own... + var resultSubj = new Bom(); + + // Add reference to this currently running build of cyclonedx-cli + // (likely) and this cyclonedx-dotnet-library into the metadata/tools + // of the merged BOM document. After all - any bugs appearing due + // to merge routines are our own and should be trackable... + // Per https://stackoverflow.com/a/36351902/4715872 : + // Use System.Reflection.Assembly.GetExecutingAssembly() + // to get the assembly (that this line of code is in), or + // use System.Reflection.Assembly.GetEntryAssembly() to + // get the assembly your project started with (most likely + // this is your app). In multi-project solutions this is + // something to keep in mind! + Tool toolThisLibrary = new Tool + { + Vendor = "OWASP Foundation", + Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" + Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() + }; + + resultSubj.Metadata = new Metadata + { + Tools = new List(new [] {toolThisLibrary}) + }; + + // At worst, these would dedup away?.. + string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar + if (toolThisScriptName != toolThisLibrary.Name) + { + Tool toolThisScript = new Tool + { + Name = toolThisScriptName, + Vendor = (toolThisScriptName.ToLower().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Version = Assembly.GetEntryAssembly().GetName().Version.ToString() + }; + resultSubj.Metadata.Tools.Add(toolThisScript); + } + + if (bomSubject is null) { - var emptyBom = new Bom(); - result = FlatMerge(emptyBom, result, safeStrategy); + result = FlatMerge(resultSubj, result, safeStrategy); } else { // use the params provided if possible: prepare a new document // with desired "metadata/component" and merge differing data // from earlier collected result into this structure. - var resultSubj = new Bom(); - resultSubj.Metadata.Component = bomSubject; resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); result = FlatMerge(resultSubj, result, safeStrategy); From 15bbfe01fea368d7c3e5accdbd662888f635f015 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 17:34:41 +0200 Subject: [PATCH 177/285] BomEntityListMergeHelper: be sure to use info from list1 if it is present and unique Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cd956a5e..991c9d2b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -223,10 +223,10 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (var item0 in list1) { + bool resMerge = false; for (int i=0; i < result.Count; i++) { var item1 = result[i]; - bool resMerge; if (methodMergeWith != null) { resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0}); @@ -241,6 +241,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat break; // item2 merged into result[item1] or already equal to it } } + + if (!resMerge) + { + result.Add(item0); + } } } From e131cd02ba8f86c2f0690e177c5224bccaff218e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 17:34:41 +0200 Subject: [PATCH 178/285] BomEntityListMergeHelper: be sure to use info from list1 if it is present and unique Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cd956a5e..991c9d2b 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -223,10 +223,10 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat { foreach (var item0 in list1) { + bool resMerge = false; for (int i=0; i < result.Count; i++) { var item1 = result[i]; - bool resMerge; if (methodMergeWith != null) { resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0}); @@ -241,6 +241,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat break; // item2 merged into result[item1] or already equal to it } } + + if (!resMerge) + { + result.Add(item0); + } } } From 48cc06fb6ad23669110a3a2d3500e378919ceb30 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 16:47:04 +0200 Subject: [PATCH 179/285] Merge.cs: inject this cyclonedx-cli script into metadata/tools[] Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index fc58b285..0849769f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using CycloneDX; using CycloneDX.Models; using CycloneDX.Models.Vulnerabilities; @@ -197,21 +198,54 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. - // TODO: Add reference to this build of cyclonedx-cli to the - // metadata/tools of the merged BOM document. After all - any bugs - // due to merge routines are our own... + var resultSubj = new Bom(); + + // Add reference to this currently running build of cyclonedx-cli + // (likely) and this cyclonedx-dotnet-library into the metadata/tools + // of the merged BOM document. After all - any bugs appearing due + // to merge routines are our own and should be trackable... + // Per https://stackoverflow.com/a/36351902/4715872 : + // Use System.Reflection.Assembly.GetExecutingAssembly() + // to get the assembly (that this line of code is in), or + // use System.Reflection.Assembly.GetEntryAssembly() to + // get the assembly your project started with (most likely + // this is your app). In multi-project solutions this is + // something to keep in mind! + Tool toolThisLibrary = new Tool + { + Vendor = "OWASP Foundation", + Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" + Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() + }; + + resultSubj.Metadata = new Metadata + { + Tools = new List(new [] {toolThisLibrary}) + }; + + // At worst, these would dedup away?.. + string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar + if (toolThisScriptName != toolThisLibrary.Name) + { + Tool toolThisScript = new Tool + { + Name = toolThisScriptName, + Vendor = (toolThisScriptName.ToLower().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Version = Assembly.GetEntryAssembly().GetName().Version.ToString() + }; + resultSubj.Metadata.Tools.Add(toolThisScript); + } + + if (bomSubject is null) { - var emptyBom = new Bom(); - result = FlatMerge(emptyBom, result, safeStrategy); + result = FlatMerge(resultSubj, result, safeStrategy); } else { // use the params provided if possible: prepare a new document // with desired "metadata/component" and merge differing data // from earlier collected result into this structure. - var resultSubj = new Bom(); - resultSubj.Metadata.Component = bomSubject; resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); result = FlatMerge(resultSubj, result, safeStrategy); From 3044b8d8be908a3c777b0b9aa1a6d010295167bf Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 18:42:52 +0200 Subject: [PATCH 180/285] Address codacy CI warnings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 14 ++++++++++++-- src/CycloneDX.Utils/Merge.cs | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 268511f5..727fc7be 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -130,7 +130,12 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } // Adapted from https://stackoverflow.com/a/76523292/4715872 - public void SortByAscending(List list, Func selector, IComparer comparer = null) + public void SortByAscending(List list, Func selector) + { + SortByAscending(list, selector, null); + } + + public void SortByAscending(List list, Func selector, IComparer comparer) { if (comparer is null) { @@ -139,7 +144,12 @@ public void SortByAscending(List list, Func selector, ICompare list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); } - public void SortByDescending(List list, Func selector, IComparer comparer = null) + public void SortByDescending(List list, Func selector) + { + SortByDescending(list, selector, null); + } + + public void SortByDescending(List list, Func selector, IComparer comparer) { if (comparer is null) { diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 0849769f..3f08900a 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -230,7 +230,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) Tool toolThisScript = new Tool { Name = toolThisScriptName, - Vendor = (toolThisScriptName.ToLower().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), Version = Assembly.GetEntryAssembly().GetName().Version.ToString() }; resultSubj.Metadata.Tools.Add(toolThisScript); From 9049efa25d4951fd33d975d86620d601617d52a9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 20:35:59 +0200 Subject: [PATCH 181/285] Merge.cs: account different object trees which wield a "BomRef" to make sure we do not lose any Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 156 +++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3f08900a..92656182 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using CycloneDX; using CycloneDX.Models; @@ -183,6 +184,16 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) BomEntityListMergeHelperStrategy quickStrategy = BomEntityListMergeHelperStrategy.Default(); quickStrategy.useBomEntityMerge = false; + // Sanity-check: we will do evil things in Components.MergeWith() + // among others, and hash-code based quick deduplication, which + // may potentially lead to loss of info. Keep track of "bom-ref" + // values we had incoming, and what we would see in the merged + // document eventually. + // TODO: Adapt if we would later rename conflicting entries on + // the fly. These dictionaries can help actually. See details in + // https://github.com/CycloneDX/cyclonedx-dotnet-library/pull/245#issuecomment-1686079370 + Dictionary dictBomRefsInput = CountBomRefs(result); + // Note: we were asked to "merge" and so we do, per principle of // least surprise - even if there is just one entry in boms[] so // we might be inclined to skip the loop. Resulting document WILL @@ -190,6 +201,8 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) int countBoms = 0; foreach (var bom in boms) { + if (countBoms > 1) + CountBomRefs(bom, ref dictBomRefsInput); result = FlatMerge(result, bom, quickStrategy); countBoms++; } @@ -275,6 +288,13 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result = CleanupEmptyLists(result); result = CleanupSortLists(result); + // Final sanity-check: + Dictionary dictBomRefsResult = CountBomRefs(result); + if (!Enumerable.SequenceEqual(dictBomRefsResult.Keys.OrderBy(e => e), dictBomRefsInput.Keys.OrderBy(e => e))) + { + Console.WriteLine("WARNING: Different sets of 'bom-ref' in the resulting document vs. original input files!"); + } + return result; } @@ -559,6 +579,142 @@ public static Bom CleanupSortLists(Bom result) return result; } + // Currently our MergeWith() logic has potential to mess with + // Component bom entities (later maybe more), and generally + // the document-wide uniqueness of BomRefs is a sore point, so + // we want them all accounted "before and after" the (flat) merge. + // Code below reuses the same dictionary object as initialized + // once for the Bom document's caller, to go faster about it: + private static void BumpDictCounter(T key, ref Dictionary dict) { + if (dict.ContainsKey(key)) { + dict[key]++; + return; + } + dict[key] = 1; + } + + private static void CountBomRefs(Component obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + if (obj.Components != null && obj.Components.Count > 0) + { + foreach (Component child in obj.Components) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree != null) + { + if (obj.Pedigree.Ancestors != null && obj.Pedigree.Ancestors.Count > 0) + { + foreach (Component child in obj.Pedigree.Ancestors) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree.Descendants != null && obj.Pedigree.Descendants.Count > 0) + { + foreach (Component child in obj.Pedigree.Descendants) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree.Variants != null && obj.Pedigree.Variants.Count > 0) + { + foreach (Component child in obj.Pedigree.Variants) + { + CountBomRefs(child, ref dict); + } + } + } + } + + private static void CountBomRefs(Service obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + if (obj.Services != null && obj.Services.Count > 0) + { + foreach (Service child in obj.Services) + { + CountBomRefs(child, ref dict); + } + } + } + + private static void CountBomRefs(Vulnerability obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + // Note: Vulnerability objects are not nested (as of CDX 1.4) + } + + private static void CountBomRefs(Bom bom, ref Dictionary dict) { + if (bom is null) + { + return; + } + + if (bom.Metadata?.Component != null) { + CountBomRefs(bom.Metadata.Component, ref dict); + } + + if (bom.Components != null && bom.Components.Count > 0) + { + foreach (Component child in bom.Components) + { + CountBomRefs(child, ref dict); + } + } + + if (bom.Services != null && bom.Services.Count > 0) + { + foreach (Service child in bom.Services) + { + CountBomRefs(child, ref dict); + } + } + + if (bom.Vulnerabilities != null && bom.Vulnerabilities.Count > 0) + { + foreach (Vulnerability child in bom.Vulnerabilities) + { + CountBomRefs(child, ref dict); + } + } + } + + private static Dictionary CountBomRefs(Bom bom) { + var dict = new Dictionary(); + CountBomRefs(bom, ref dict); + return dict; + } + private static string NamespacedBomRef(Component bomSubject, string bomRef) { return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); From 437616719a3d554af2d4c30b9241fe83215bfeb1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 20:36:41 +0200 Subject: [PATCH 182/285] Merge.cs: comment away INJECTED-ERROR-FOR-TESTING Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 92656182..be9c011d 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -201,7 +201,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) int countBoms = 0; foreach (var bom in boms) { - if (countBoms > 1) + // INJECTED-ERROR-FOR-TESTING // if (countBoms > 1) CountBomRefs(bom, ref dictBomRefsInput); result = FlatMerge(result, bom, quickStrategy); countBoms++; From 35bc42b53074f926459f00b51c0393ec7b23b028 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 20:41:09 +0200 Subject: [PATCH 183/285] Merge.cs: account the "BomRef" of the bomSubject optionally coming from CLI arguments Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index be9c011d..cf5c80ea 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -261,6 +261,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // from earlier collected result into this structure. resultSubj.Metadata.Component = bomSubject; resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + CountBomRefs(resultSubj, ref dictBomRefsInput); result = FlatMerge(resultSubj, result, safeStrategy); var mainDependency = new Dependency(); From 65e0397230bd660d477139d3ff63ddb6f651309c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 20:46:01 +0200 Subject: [PATCH 184/285] BomEntity.GetHashCode(): mix length of the serialized representation into its hash for a bit more randomness Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 991c9d2b..a8bbed37 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -675,9 +675,18 @@ public override bool Equals(Object obj) return this.Equals((BomEntity)obj); } + /// + /// Returns hash code of the string returned by + /// `this.SerializeEntity()` (typically a compact + /// JSON representation) plus the length of this + /// string to randomize it a bit against hash + /// collisions. Never saw those, but just in case. + /// + /// Int hash code public override int GetHashCode() { - return this.SerializeEntity().GetHashCode(); + string ser = this.SerializeEntity(); + return ser.GetHashCode() + ser.Length; } /// From 6c6cf70a029d96fb310990d5d735a8d98ee5f9d8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 21:04:33 +0200 Subject: [PATCH 185/285] Merge.cs: comment away INJECTED-ERROR-FOR-TESTING - reword for CI Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index cf5c80ea..a62ce18f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -201,7 +201,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) int countBoms = 0; foreach (var bom in boms) { - // INJECTED-ERROR-FOR-TESTING // if (countBoms > 1) + // INJECTED-ERROR-FOR-TESTING // if (countBoms > 1) then ... CountBomRefs(bom, ref dictBomRefsInput); result = FlatMerge(result, bom, quickStrategy); countBoms++; From 9946f2eb587b18be9148649b19d5de04617e62b0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 21 Aug 2023 21:04:33 +0200 Subject: [PATCH 186/285] Merge.cs: comment away INJECTED-ERROR-FOR-TESTING - reword for CI Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index cf5c80ea..863af6a5 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -201,7 +201,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) int countBoms = 0; foreach (var bom in boms) { - // INJECTED-ERROR-FOR-TESTING // if (countBoms > 1) + // INJECTED-ERROR-FOR-TESTING // if countBoms > 1 then ... CountBomRefs(bom, ref dictBomRefsInput); result = FlatMerge(result, bom, quickStrategy); countBoms++; From 5296596ece5ab8fb6281be4577fdb109c61e7d29 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 10:26:02 +0200 Subject: [PATCH 187/285] Component.cs: Equivalent(): refine based on spec and practical identifying fields Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 55aecb09..955a68dc 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -205,7 +205,37 @@ public bool NonNullableModified public bool Equivalent(Component obj) { - return (!(obj is null) && this.BomRef == obj.BomRef); + // "this" is not null ever, so the counterpart also should not be :) + if (obj is null) + { + return false; + } + + // By spec, as of CDX 1.4, "type" and "name" are the two required + // properties. Commonly, a "version" or "purl" makes sense to be + // sure about (not-)sameness of two components. And historically, + // for many computer-generated BOMs the "bom-ref" is representative. + // Notably, we care about BomRef because we might refer to this + // entity from others in the same document (e.g. Dependencies[]), + // and BOM references are done by this value. + // NOTE: If two otherwise identical components are refferred to + // by different BomRef values - so be it, Bom duplicates remain. + if (this.Type == obj.Type // No nullness check here, or we get: error CS0037: Cannot convert null to 'Component.Classification' because it is a non-nullable value type + && !(this.Name is null) && !(obj.Name is null) && this.Name == obj.Name + && (this.Version is null || obj.Version is null || this.Version == obj.Version) + && (this.Purl is null || obj.Purl is null || this.Purl == obj.Purl) + && (this.BomRef is null || obj.BomRef is null || this.BomRef == obj.BomRef) + ) + { + // These two seem equivalent enough to go on with the more + // expensive logic such as MergeWith() which may ultimately + // reject the merge request - based on some unresolvable + // incompatibility in some other data fields or lists: + return true; + } + + // Could not prove equivalence, err on the safe side: + return false; } public bool MergeWith(Component obj) From 0ca4680dfeca2d460cc687e62b78e5d341427bef Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 10:26:02 +0200 Subject: [PATCH 188/285] Component.cs: Equivalent(): refine based on spec and practical identifying fields Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 55aecb09..955a68dc 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -205,7 +205,37 @@ public bool NonNullableModified public bool Equivalent(Component obj) { - return (!(obj is null) && this.BomRef == obj.BomRef); + // "this" is not null ever, so the counterpart also should not be :) + if (obj is null) + { + return false; + } + + // By spec, as of CDX 1.4, "type" and "name" are the two required + // properties. Commonly, a "version" or "purl" makes sense to be + // sure about (not-)sameness of two components. And historically, + // for many computer-generated BOMs the "bom-ref" is representative. + // Notably, we care about BomRef because we might refer to this + // entity from others in the same document (e.g. Dependencies[]), + // and BOM references are done by this value. + // NOTE: If two otherwise identical components are refferred to + // by different BomRef values - so be it, Bom duplicates remain. + if (this.Type == obj.Type // No nullness check here, or we get: error CS0037: Cannot convert null to 'Component.Classification' because it is a non-nullable value type + && !(this.Name is null) && !(obj.Name is null) && this.Name == obj.Name + && (this.Version is null || obj.Version is null || this.Version == obj.Version) + && (this.Purl is null || obj.Purl is null || this.Purl == obj.Purl) + && (this.BomRef is null || obj.BomRef is null || this.BomRef == obj.BomRef) + ) + { + // These two seem equivalent enough to go on with the more + // expensive logic such as MergeWith() which may ultimately + // reject the merge request - based on some unresolvable + // incompatibility in some other data fields or lists: + return true; + } + + // Could not prove equivalence, err on the safe side: + return false; } public bool MergeWith(Component obj) From b61ac4428699ff6d37d66c72841489c06220f10f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 14:34:22 +0200 Subject: [PATCH 189/285] ListMergeHelper: refactor SortBy*() methods to minimize the amount of implementations behind various parameter-set variants Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 727fc7be..bc639c7c 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -132,30 +132,44 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Adapted from https://stackoverflow.com/a/76523292/4715872 public void SortByAscending(List list, Func selector) { - SortByAscending(list, selector, null); + SortByImpl(true, list, selector, null); } public void SortByAscending(List list, Func selector, IComparer comparer) { - if (comparer is null) - { - comparer = Comparer.Default; - } - list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + SortByImpl(true, list, selector, comparer); } public void SortByDescending(List list, Func selector) { - SortByDescending(list, selector, null); + SortByImpl(false, list, selector, null); } public void SortByDescending(List list, Func selector, IComparer comparer) { + SortByImpl(false, list, selector, comparer); + } + + public void SortByImpl(bool ascending, List list, Func selector, IComparer comparer) + { + if (list is null || list.Count < 2) + { + // No-op quickly for null, empty or single-item lists + return; + } if (comparer is null) { comparer = Comparer.Default; } - list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + + if (ascending) + { + list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + } + else + { + list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + } } } } From 33a89ecb3738be301fbefab5a94a99dbc9c1992e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 15:35:58 +0200 Subject: [PATCH 190/285] BomEntity: cache info about Sort() and Reverse() methods of constructed list types Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a8bbed37..76b5081d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -330,6 +330,8 @@ public class BomEntityListReflection public MethodInfo methodAdd { get; set; } public MethodInfo methodAddRange { get; set; } public MethodInfo methodGetItem { get; set; } + public MethodInfo methodSort { get; set; } + public MethodInfo methodReverse { get; set; } } public class BomEntityListMergeHelperReflection @@ -410,6 +412,8 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); + dict[type].methodSort = constructedListType.GetMethod("Sort"); + dict[type].methodReverse = constructedListType.GetMethod("Reverse"); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. From 01c22153997af842e9376aa97469c545b945454e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 15:37:09 +0200 Subject: [PATCH 191/285] BomEntity: implement a recursible static NormalizeList() to sort lists of BomEntity-derived types (optionally recursing into their properties which are lists of something) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 208 ++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 76b5081d..97632729 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -412,8 +412,11 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); - dict[type].methodSort = constructedListType.GetMethod("Sort"); - dict[type].methodReverse = constructedListType.GetMethod("Reverse"); + + // Use the default no-arg implementations here explicitly, + // to avoid an System.Reflection.AmbiguousMatchException: + dict[type].methodSort = constructedListType.GetMethod("Sort", 0, new Type[] {}); + dict[type].methodReverse = constructedListType.GetMethod("Reverse", 0, new Type[] {}); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. @@ -618,6 +621,29 @@ public class BomEntity : IEquatable return ImmutableDictionary.CreateRange(dict); }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom static NormalizeList() method + /// implementations (if present) for sorting=>normalization of lists + /// of that BomEntity-derived type, prepared startically at start time. + /// + public static readonly ImmutableDictionary KnownTypeNormalizeList = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + if (method != null) + { + dict[type] = method; + } + } + return ImmutableDictionary.CreateRange(dict); + }) (); + protected BomEntity() { // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") @@ -719,6 +745,184 @@ public bool Equivalent(BomEntity obj) return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } + /// + /// In-place normalization of a list of BomEntity-derived type. + /// Derived classes can implement this as a sort by one or more + /// of specific properties (e.g. name or bom-ref). Note that + /// handling of the "recursive" option is commonly handled in + /// the base-class method, via which these should be called. + /// Being a static method, those in derived classes are not + /// overrides for the PoV of the language. + /// + /// Ordering proposed in these methods is an educated guess. + /// Main purpose for this is to have some consistently ordered + /// serialized BomEntity lists for the purposes of comparison + /// and compression. + /// + /// TODO: this should really be offloaded as lambdas into the + /// BomEntity-derived classes themselves, but I've struggled + /// to cast the right magic spells at C# to please its gods. + /// In particular, the ValueTuple used in selector signature is + /// both generic for the values' types (e.g. ), + /// and for their amount in the tuple (0, 1, 2, ... explicitly + /// stated). So this is the next best thing... + /// + public static void NormalizeList(bool ascending, bool recursive, List list) + { + if (list is null || list.Count < 2) + { + // No-op quickly for null, empty or single-item lists + return; + } + + Type thisType = list[0].GetType(); + + if (recursive) + { + // Look into properties of each currently listed BomEntity-derived + // type instance, so if there are further lists - sort them similarly. + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[thisType]; + foreach (PropertyInfo property in properties) + { + if (property.PropertyType == typeof(List) || property.PropertyType.ToString().StartsWith("System.Collections.Generic.List")) + { + // Re-use these learnings while we iterate all original + // list items regarding the specified sub-list property: + Type LType = null; + Type TType = null; + PropertyInfo propCount = null; + MethodInfo methodGetItem = null; + MethodInfo methodSort = null; + MethodInfo methodReverse = null; + MethodInfo methodNormalizeSubList = null; + bool retryMethodNormalizeSubList = true; + + foreach(var obj in list) + { + if (obj is null) + { + continue; + } + + try + { + // Use cached info where available for all the + // list and list-item types and methods involved. + + // Get the (list) property of the originally iterated + // BomEntity-derived item from our original list: + var propValObj = property.GetValue(obj); + + // Is that sub-list trivial enough to skip? + if (propValObj is null) + { + continue; + } + + if (LType == null) + { + LType = propValObj.GetType(); + } + + // Learn how to query that LType sort of lists: + if (methodGetItem == null || propCount == null || methodSort == null || methodReverse == null) + { + if (BomEntity.KnownEntityTypeLists.TryGetValue(LType, out BomEntityListReflection refInfo)) + { + propCount = refInfo.propCount; + methodGetItem = refInfo.methodGetItem; + methodSort = refInfo.methodSort; + methodReverse = refInfo.methodReverse; + } + else + { + propCount = LType.GetProperty("Count"); + methodGetItem = LType.GetMethod("get_Item"); + methodSort = LType.GetMethod("Sort"); + methodReverse = LType.GetMethod("Reverse"); + } + + if (methodGetItem == null || propCount == null || methodSort == null || methodReverse == null) + { + // is this really a LIST - it lacks a get_Item() or other methods, or a Count property + continue; + } + } + + // Is that sub-list trivial enough to skip? + int propValObjCount = (int)propCount.GetValue(propValObj, null); + if (propValObjCount < 2) + { + continue; + } + + // Type of items in that sub-list: + if (TType == null) + { + TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); + } + + // Learn how to sort the sub-list of those item types: + if (methodNormalizeSubList == null && retryMethodNormalizeSubList) + { + if (!KnownTypeNormalizeList.TryGetValue(TType, out var methodNormalizeSubListTmp)) + { + methodNormalizeSubListTmp = null; + retryMethodNormalizeSubList = false; + } + methodNormalizeSubList = methodNormalizeSubListTmp; + } + + if (methodNormalizeSubList != null) + { + // call static NormalizeList(..., List obj.propValObj) + methodNormalizeSubList.Invoke(null, new object[] {ascending, recursive, propValObj}); + } + else + { + // Default-sort a common sub-list directly (no recursion) + methodSort.Invoke(propValObj, null); + if (!ascending) + { + methodReverse.Invoke(propValObj, null); + } + } + } + catch (System.InvalidOperationException) + { + // property.GetValue(obj) failed + continue; + } + catch (System.Reflection.TargetInvocationException) + { + // property.GetValue(obj) failed + continue; + } + } + } + } + } + + if (KnownTypeNormalizeList.TryGetValue(thisType, out var methodNormalizeList)) + { + // Note we do not check for null/type of "obj" at this point + // since the derived classes define the logic of equivalence + // (possibly to other entity subtypes as well). + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + return; + } + + // Expensive but reliable default implementation (modulo differently + // sorted lists of identical item sets inside the otherwise identical + // objects -- but currently spec seems to mean ordered collections); + // classes are welcome to implement theirs eventually or switch cases + // above currently. + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, list, + o => (o?.SerializeEntity()), + null); + } + /// /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false From 3073e00bf704a623abe77458f951b2a348e37449 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 20:05:29 +0200 Subject: [PATCH 192/285] ListMergeHelper.SortByImpl(): support calling BomEntity.NormalizeList() and pass the "recurse" option to it and back from it where applicable (for having the actual recursion) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 72 ++++++++++++++++++++++++-- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index bc639c7c..326f1a68 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -130,33 +130,95 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } // Adapted from https://stackoverflow.com/a/76523292/4715872 + public void SortByAscending(List list) + { + SortByImpl(true, false, list, null, null); + } + + public void SortByAscending(List list, bool recursive) + { + SortByImpl(true, recursive, list, null, null); + } + public void SortByAscending(List list, Func selector) { - SortByImpl(true, list, selector, null); + SortByImpl(true, false, list, selector, null); } public void SortByAscending(List list, Func selector, IComparer comparer) { - SortByImpl(true, list, selector, comparer); + SortByImpl(true, false, list, selector, comparer); + } + + public void SortByDescending(List list) + { + SortByImpl(false, false, list, null, null); + } + + public void SortByDescending(List list, bool recursive) + { + SortByImpl(false, recursive, list, null, null); } public void SortByDescending(List list, Func selector) { - SortByImpl(false, list, selector, null); + SortByImpl(false, false, list, selector, null); } public void SortByDescending(List list, Func selector, IComparer comparer) { - SortByImpl(false, list, selector, comparer); + SortByImpl(false, false, list, selector, comparer); } - public void SortByImpl(bool ascending, List list, Func selector, IComparer comparer) + /// + /// Implementation of the sort algorithm. + /// Special handling for BomEntity-derived objects, including + /// optional recursion to have them sort their list-of-something + /// properties. + /// + /// ValueTuple of function parameters returned by selector lambda + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + /// lambda to select a tuple of properties to sort by + /// null for default, or a custom comparer + public void SortByImpl(bool ascending, bool recursive, List list, Func selector, IComparer comparer) { if (list is null || list.Count < 2) { // No-op quickly for null, empty or single-item lists return; } + + // Ordering proposed in those NormalizeList() implementations + // is an educated guess. Main purpose for this is to have + // consistently ordered serialized BomEntity-derived type + // lists for the purposes of comparison and compression. + if (selector is null && typeof(BomEntity).IsInstanceOfType(list[0])) + { + // This should really be offloaded as lambdas into the + // BomEntity-derived classes themselves, but I've struggled + // to cast the right magic spells at C# to please its gods. + // In particular, the ValueTuple used in selector signature is + // both generic for the values' types (e.g. ), + // and for their amount in the tuple (0, 1, 2, ... explicitly + // stated). So this is the next best thing... + + // Alas, C# won't let us just call + // BomEntity.NormalizeList(ascending, recursive, (List)list) or + // something as simple, so here it goes - some more reflection: + var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + + if (methodNormalizeList != null) + { + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + } // else keep it as was? no good cause for an exception?.. + + return; + } + if (comparer is null) { comparer = Comparer.Default; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 97632729..aadfd926 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -918,7 +918,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // classes are welcome to implement theirs eventually or switch cases // above currently. var sortHelper = new ListMergeHelper(); - sortHelper.SortByImpl(ascending, list, + sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); } From 375a4a3a8051d81b0bf302d807c73adb4899f391 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:50:17 +0200 Subject: [PATCH 193/285] ListMergeHelper: hack around type-less SortByAscending()/SortByDescending() variants Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 326f1a68..e7c896da 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -130,14 +130,14 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } // Adapted from https://stackoverflow.com/a/76523292/4715872 - public void SortByAscending(List list) + public void SortByAscending(List list) { - SortByImpl(true, false, list, null, null); + SortByImpl(true, false, list, null, null); } - public void SortByAscending(List list, bool recursive) + public void SortByAscending(List list, bool recursive) { - SortByImpl(true, recursive, list, null, null); + SortByImpl(true, recursive, list, null, null); } public void SortByAscending(List list, Func selector) @@ -150,14 +150,14 @@ public void SortByAscending(List list, Func selector, ICompare SortByImpl(true, false, list, selector, comparer); } - public void SortByDescending(List list) + public void SortByDescending(List list) { - SortByImpl(false, false, list, null, null); + SortByImpl(false, false, list, null, null); } - public void SortByDescending(List list, bool recursive) + public void SortByDescending(List list, bool recursive) { - SortByImpl(false, recursive, list, null, null); + SortByImpl(false, recursive, list, null, null); } public void SortByDescending(List list, Func selector) From 57d45eed428bddf07d5794fa5dd34d6fae40c58f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:52:29 +0200 Subject: [PATCH 194/285] BomEntity: introduce interface IBomEntity to fiddle with inheritance UPDATE: Did not help make List easier. The List<> type does not really care about data types inside, just rejects them whichever way. So reflection stays... Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index aadfd926..e933e90d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -341,6 +341,17 @@ public class BomEntityListMergeHelperReflection public Object helperInstance { get; set; } } + /// + /// Primarily used to make life with List a bit + /// easier than List when used against derived types. + /// + public interface IBomEntity + { + bool Equivalent(BomEntity obj); + bool MergeWith(BomEntity obj); + string SerializeEntity(); + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -350,7 +361,7 @@ public class BomEntityListMergeHelperReflection /// and to define the logic for merge-ability of such objects while coding much /// of the logical scaffolding only once. /// - public class BomEntity : IEquatable + public class BomEntity : IEquatable, IBomEntity { // Keep this info initialized once to cut down on overheads of reflection // when running in our run-time loops. @@ -396,7 +407,9 @@ public class BomEntity : IEquatable new Func>(() => { Dictionary dict = new Dictionary(); - foreach (var type in KnownEntityTypes) + List KnownEntityTypesPlus = new List(KnownEntityTypes); + KnownEntityTypesPlus.Add(typeof(IBomEntity)); + foreach (var type in KnownEntityTypesPlus) { // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: @@ -654,7 +667,7 @@ protected BomEntity() /// Calls our standard CycloneDX.Json.Serializer to use /// its common options in particular. /// - internal string SerializeEntity() + public string SerializeEntity() { // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class @@ -767,7 +780,7 @@ public bool Equivalent(BomEntity obj) /// and for their amount in the tuple (0, 1, 2, ... explicitly /// stated). So this is the next best thing... /// - public static void NormalizeList(bool ascending, bool recursive, List list) + public static void NormalizeList(bool ascending, bool recursive, List list) { if (list is null || list.Count < 2) { @@ -917,7 +930,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // objects -- but currently spec seems to mean ordered collections); // classes are welcome to implement theirs eventually or switch cases // above currently. - var sortHelper = new ListMergeHelper(); + var sortHelper = new ListMergeHelper(); sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); From 2208616b350102b38f63461fc4be8343639be918 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:55:08 +0200 Subject: [PATCH 195/285] BomEntity: extend KnownTypeNormalizeList detection to cover IBomEntity among fallbacks, and a more specific list type for derived classes Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e933e90d..b870fedf 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -646,7 +646,41 @@ public class BomEntity : IEquatable, IBomEntity Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { - var method = type.GetMethod("NormalizeList", + MethodInfo method = null; + + if (BomEntity.KnownEntityTypeLists.TryGetValue(type, out BomEntityListReflection refInfoListType)) + { + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), refInfoListType.genericType }); + if (method != null) + { + dict[type] = method; + continue; + } + } + + // Try interface default + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(IList) }); + if (method != null) + { + dict[type] = method; + continue; + } + + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + if (method != null) + { + dict[type] = method; + continue; + } + + // Try class default + method = type.GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, new [] { typeof(bool), typeof(bool), typeof(List) }); if (method != null) From 157d6603310fd8c896ae01f63fefc2ac845335f1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:56:51 +0200 Subject: [PATCH 196/285] BomEntity and ListMergeHelper: fix calling of NormalizeList() and back to SortByImpl() ...if having to create copies of lists as a fake cast for List and back counts as a "fix". But hey, it works! Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 77 +++++++++++++++++++++++++- src/CycloneDX.Core/Models/BomEntity.cs | 33 +++++++++-- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index e7c896da..2c5c2790 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -204,16 +204,89 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // and for their amount in the tuple (0, 1, 2, ... explicitly // stated). So this is the next best thing... +/* + switch (typeof(TKey)) { + case Tool: + SortByImpl(ascending, list, + o => (o?.Vendor, o?.Name, o?.Version), + comparer); + return; + + case typeof(Component): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), + comparer); + return; + + case typeof(Service): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Group, o?.Name, o?.Version), + comparer); + return; + + case typeof(ExternalReference): + SortByImpl(ascending, list, + o => (o?.Url, o?.Type), + comparer); + return; + + case typeof(Dependency): + SortByImpl(ascending, list, + o => (o?.Ref), + comparer); + return; + + case typeof(Composition): + SortByImpl(ascending, list, + o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), + comparer); + return; + + case typeof(Vulnerability): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), + comparer); + return; + + default: + // Expensive but reliable (modulo differently sorted lists + // of identical item sets inside the otherwise identical + // objects); classes are welcome to implement theirs eventually + // or switch cases above currently. + SortByImplBomEntity(ascending, list, + o => (o?.SerializeEntity()), + comparer); + return; + } +*/ + // Alas, C# won't let us just call // BomEntity.NormalizeList(ascending, recursive, (List)list) or // something as simple, so here it goes - some more reflection: var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); + new [] { typeof(bool), typeof(bool), typeof(List) }); if (methodNormalizeList != null) { - methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(IBomEntity), out BomEntityListReflection refInfoListInterface)) + { + if (BomEntity.KnownEntityTypeLists.TryGetValue(list[0].GetType(), out BomEntityListReflection refInfoListType)) + { + // Gotta make ugly cast copies there and back: + List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); + refInfoListInterface.methodAddRange.Invoke(helper, new object[] {list}); + + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, helper}); + + // Populate back the original list object: + list.Clear(); + foreach (var item in helper) + { + refInfoListType.methodAdd.Invoke(list, new object[] {item}); + } + } + } } // else keep it as was? no good cause for an exception?.. return; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index b870fedf..45587f50 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -812,7 +812,9 @@ public bool Equivalent(BomEntity obj) /// In particular, the ValueTuple used in selector signature is /// both generic for the values' types (e.g. ), /// and for their amount in the tuple (0, 1, 2, ... explicitly - /// stated). So this is the next best thing... + /// stated). So this is the next best thing... even if highly + /// inefficient to copy lists from one type to another as a + /// fake cast. At least it works!.. /// public static void NormalizeList(bool ascending, bool recursive, List list) { @@ -952,11 +954,30 @@ public static void NormalizeList(bool ascending, bool recursive, List Date: Tue, 22 Aug 2023 22:58:06 +0200 Subject: [PATCH 197/285] BomEntity: create simpler NormalizeList() aliases with default settings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 45587f50..e28f84c4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -992,6 +992,26 @@ public static void NormalizeList(bool ascending, bool recursive, List + /// See NormalizeList(); this variant defaults "ascending=true" + /// + /// Should this recurse into child-object properties which are sub-lists? + /// + public static void NormalizeList(bool recursive, List list) + { + NormalizeList(true, recursive, list); + } + + /// + /// + /// See NormalizeList(); this variant defaults "ascending=true" + /// and "recursive=false" to only normalize the given list itself. + /// + /// + public static void NormalizeList(List list) + { + NormalizeList(true, false, list); + } + /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false /// objects are not (returns false), and for others (equivalent but From 9843d5d7fd9e822ed4445e7a4bab4b27ff936e86 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 23:00:02 +0200 Subject: [PATCH 198/285] Merge: simplify CleanupSortLists() by not keeping sort-filters in the method - move them to class definitions and use new BomEntity.NormalizeList() codepath Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 56 ------------------- src/CycloneDX.Core/Models/Component.cs | 17 ++++++ src/CycloneDX.Core/Models/Composition.cs | 19 ++++++- src/CycloneDX.Core/Models/Dependency.cs | 19 ++++++- .../Models/ExternalReference.cs | 17 ++++++ src/CycloneDX.Core/Models/Service.cs | 17 ++++++ src/CycloneDX.Core/Models/Tool.cs | 17 ++++++ .../Models/Vulnerabilities/Vulnerability.cs | 17 ++++++ src/CycloneDX.Utils/Merge.cs | 21 ++++--- 9 files changed, 135 insertions(+), 65 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 2c5c2790..fa8ec8c4 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -204,62 +204,6 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // and for their amount in the tuple (0, 1, 2, ... explicitly // stated). So this is the next best thing... -/* - switch (typeof(TKey)) { - case Tool: - SortByImpl(ascending, list, - o => (o?.Vendor, o?.Name, o?.Version), - comparer); - return; - - case typeof(Component): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), - comparer); - return; - - case typeof(Service): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Group, o?.Name, o?.Version), - comparer); - return; - - case typeof(ExternalReference): - SortByImpl(ascending, list, - o => (o?.Url, o?.Type), - comparer); - return; - - case typeof(Dependency): - SortByImpl(ascending, list, - o => (o?.Ref), - comparer); - return; - - case typeof(Composition): - SortByImpl(ascending, list, - o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), - comparer); - return; - - case typeof(Vulnerability): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), - comparer); - return; - - default: - // Expensive but reliable (modulo differently sorted lists - // of identical item sets inside the otherwise identical - // objects); classes are welcome to implement theirs eventually - // or switch cases above currently. - SortByImplBomEntity(ascending, list, - o => (o?.SerializeEntity()), - comparer); - return; - } -*/ - // Alas, C# won't let us just call // BomEntity.NormalizeList(ascending, recursive, (List)list) or // something as simple, so here it goes - some more reflection: diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 955a68dc..0d36298f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -238,6 +238,23 @@ public bool Equivalent(Component obj) return false; } + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), + null); + } + public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index c6643738..c0081b1e 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -141,5 +141,22 @@ public void WriteXml(System.Xml.XmlWriter writer) { writer.WriteEndElement(); } } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), + null); + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 70a2e948..15471381 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -35,5 +35,22 @@ public class Dependency : BomEntity [XmlElement("dependency")] [ProtoMember(2)] public List Dependencies { get; set; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Ref), + null); + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index 84d12e5d..421ea511 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -79,5 +79,22 @@ public enum ExternalReferenceType [ProtoMember(4)] public List Hashes { get; set; } public bool ShouldSerializeHashes() { return Hashes?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Url, o?.Type), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 9de775b3..ef47f347 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -123,5 +123,22 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Group, o?.Name, o?.Version), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 6674462c..fdb9fcaa 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -46,5 +46,22 @@ public class Tool : BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Vendor, o?.Name, o?.Version), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index 372e8247..2bbe624e 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -124,5 +124,22 @@ public DateTime? Updated [ProtoMember(18)] public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), + null); + } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 863af6a5..9c760f75 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -535,46 +535,53 @@ public static Bom CleanupEmptyLists(Bom result) /// Resulting document (whether modified or not) public static Bom CleanupSortLists(Bom result) { + // Why oh why?.. error CS1503: Argument 1: cannot convert + // from 'System.Collections.Generic.List' + // to 'System.Collections.Generic.List' + // BomEntity.NormalizeList(result.Tools) -- it looks so simple! + // But at least we *can* call it, perhaps inefficiently for + // the run-time code and scaffolding, but easy to maintain + // with filter definitions now stored in classes, not here... if (result.Metadata?.Tools?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Metadata.Tools, o => (o?.Vendor, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Metadata.Tools); } if (result.Components?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Components, o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Components); } if (result.Services?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Services); } if (result.ExternalReferences?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.ExternalReferences, o => (o?.Url, o?.Type)); + sortHelper.SortByAscending(result.ExternalReferences); } if (result.Dependencies?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Dependencies, o => (o?.Ref)); + sortHelper.SortByAscending(result.Dependencies); } if (result.Compositions?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Compositions, o => (o?.Aggregate, o?.Assemblies, o?.Dependencies)); + sortHelper.SortByAscending(result.Compositions); } if (result.Vulnerabilities?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Vulnerabilities, o => (o?.BomRef, o?.Id, o?.Created, o?.Updated)); + sortHelper.SortByAscending(result.Vulnerabilities); } return result; From 568ddc288a56c0c4a973bbf75b1d10ea32fcba33 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 23:04:35 +0200 Subject: [PATCH 199/285] Drop IBomEntity as a failed and useless experiment Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 6 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 46 ++++---------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index fa8ec8c4..28920639 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -209,16 +209,16 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // something as simple, so here it goes - some more reflection: var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); + new [] { typeof(bool), typeof(bool), typeof(List) }); if (methodNormalizeList != null) { - if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(IBomEntity), out BomEntityListReflection refInfoListInterface)) + if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(BomEntity), out BomEntityListReflection refInfoListInterface)) { if (BomEntity.KnownEntityTypeLists.TryGetValue(list[0].GetType(), out BomEntityListReflection refInfoListType)) { // Gotta make ugly cast copies there and back: - List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); + List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); refInfoListInterface.methodAddRange.Invoke(helper, new object[] {list}); methodNormalizeList.Invoke(null, new object[] {ascending, recursive, helper}); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e28f84c4..b0294074 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -341,17 +341,6 @@ public class BomEntityListMergeHelperReflection public Object helperInstance { get; set; } } - /// - /// Primarily used to make life with List a bit - /// easier than List when used against derived types. - /// - public interface IBomEntity - { - bool Equivalent(BomEntity obj); - bool MergeWith(BomEntity obj); - string SerializeEntity(); - } - /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -361,7 +350,7 @@ public interface IBomEntity /// and to define the logic for merge-ability of such objects while coding much /// of the logical scaffolding only once. /// - public class BomEntity : IEquatable, IBomEntity + public class BomEntity : IEquatable { // Keep this info initialized once to cut down on overheads of reflection // when running in our run-time loops. @@ -407,9 +396,7 @@ public class BomEntity : IEquatable, IBomEntity new Func>(() => { Dictionary dict = new Dictionary(); - List KnownEntityTypesPlus = new List(KnownEntityTypes); - KnownEntityTypesPlus.Add(typeof(IBomEntity)); - foreach (var type in KnownEntityTypesPlus) + foreach (var type in KnownEntityTypes) { // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: @@ -660,25 +647,6 @@ public class BomEntity : IEquatable, IBomEntity } } - // Try interface default - method = type.GetMethod("NormalizeList", - BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(IList) }); - if (method != null) - { - dict[type] = method; - continue; - } - - method = type.GetMethod("NormalizeList", - BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); - if (method != null) - { - dict[type] = method; - continue; - } - // Try class default method = type.GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, @@ -816,7 +784,7 @@ public bool Equivalent(BomEntity obj) /// inefficient to copy lists from one type to another as a /// fake cast. At least it works!.. /// - public static void NormalizeList(bool ascending, bool recursive, List list) + public static void NormalizeList(bool ascending, bool recursive, List list) { if (list is null || list.Count < 2) { @@ -956,7 +924,7 @@ public static void NormalizeList(bool ascending, bool recursive, List(); + var sortHelper = new ListMergeHelper(); sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); @@ -996,7 +964,7 @@ public static void NormalizeList(bool ascending, bool recursive, List /// Should this recurse into child-object properties which are sub-lists? /// - public static void NormalizeList(bool recursive, List list) + public static void NormalizeList(bool recursive, List list) { NormalizeList(true, recursive, list); } @@ -1007,7 +975,7 @@ public static void NormalizeList(bool recursive, List list) /// and "recursive=false" to only normalize the given list itself. /// /// - public static void NormalizeList(List list) + public static void NormalizeList(List list) { NormalizeList(true, false, list); } From 3262d6dc4614518ea649158870134527e4e78248 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 14:34:22 +0200 Subject: [PATCH 200/285] ListMergeHelper: refactor SortBy*() methods to minimize the amount of implementations behind various parameter-set variants Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 727fc7be..bc639c7c 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -132,30 +132,44 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat // Adapted from https://stackoverflow.com/a/76523292/4715872 public void SortByAscending(List list, Func selector) { - SortByAscending(list, selector, null); + SortByImpl(true, list, selector, null); } public void SortByAscending(List list, Func selector, IComparer comparer) { - if (comparer is null) - { - comparer = Comparer.Default; - } - list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + SortByImpl(true, list, selector, comparer); } public void SortByDescending(List list, Func selector) { - SortByDescending(list, selector, null); + SortByImpl(false, list, selector, null); } public void SortByDescending(List list, Func selector, IComparer comparer) { + SortByImpl(false, list, selector, comparer); + } + + public void SortByImpl(bool ascending, List list, Func selector, IComparer comparer) + { + if (list is null || list.Count < 2) + { + // No-op quickly for null, empty or single-item lists + return; + } if (comparer is null) { comparer = Comparer.Default; } - list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + + if (ascending) + { + list.Sort((a, b) => comparer.Compare(selector(a), selector(b))); + } + else + { + list.Sort((a, b) => comparer.Compare(selector(b), selector(a))); + } } } } From f7844dc425e1ff4075eb25fd6abab97030054213 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 15:35:58 +0200 Subject: [PATCH 201/285] BomEntity: cache info about Sort() and Reverse() methods of constructed list types Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a8bbed37..76b5081d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -330,6 +330,8 @@ public class BomEntityListReflection public MethodInfo methodAdd { get; set; } public MethodInfo methodAddRange { get; set; } public MethodInfo methodGetItem { get; set; } + public MethodInfo methodSort { get; set; } + public MethodInfo methodReverse { get; set; } } public class BomEntityListMergeHelperReflection @@ -410,6 +412,8 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); + dict[type].methodSort = constructedListType.GetMethod("Sort"); + dict[type].methodReverse = constructedListType.GetMethod("Reverse"); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. From f4121e009d0951818228336b9a86775de7f8cb9a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 15:37:09 +0200 Subject: [PATCH 202/285] BomEntity: implement a recursible static NormalizeList() to sort lists of BomEntity-derived types (optionally recursing into their properties which are lists of something) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 208 ++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 76b5081d..97632729 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -412,8 +412,11 @@ public class BomEntity : IEquatable dict[type].methodGetItem = constructedListType.GetMethod("get_Item"); dict[type].methodAdd = constructedListType.GetMethod("Add", 0, new [] { type }); dict[type].methodAddRange = constructedListType.GetMethod("AddRange", 0, new [] { constructedListType }); - dict[type].methodSort = constructedListType.GetMethod("Sort"); - dict[type].methodReverse = constructedListType.GetMethod("Reverse"); + + // Use the default no-arg implementations here explicitly, + // to avoid an System.Reflection.AmbiguousMatchException: + dict[type].methodSort = constructedListType.GetMethod("Sort", 0, new Type[] {}); + dict[type].methodReverse = constructedListType.GetMethod("Reverse", 0, new Type[] {}); // Avoid: No cached info about BomEntityListReflection[System.Collections.Generic.List`1[CycloneDX.Models.ExternalReference]] // TODO: Separate dict?.. @@ -618,6 +621,29 @@ public class BomEntity : IEquatable return ImmutableDictionary.CreateRange(dict); }) (); + /// + /// Dictionary mapping classes derived from BomEntity to reflection + /// MethodInfo about their custom static NormalizeList() method + /// implementations (if present) for sorting=>normalization of lists + /// of that BomEntity-derived type, prepared startically at start time. + /// + public static readonly ImmutableDictionary KnownTypeNormalizeList = + new Func>(() => + { + Dictionary dict = new Dictionary(); + foreach (var type in KnownEntityTypes) + { + var method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + if (method != null) + { + dict[type] = method; + } + } + return ImmutableDictionary.CreateRange(dict); + }) (); + protected BomEntity() { // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") @@ -719,6 +745,184 @@ public bool Equivalent(BomEntity obj) return (!(obj is null) && (thisType == obj.GetType()) && this.Equals(obj)); } + /// + /// In-place normalization of a list of BomEntity-derived type. + /// Derived classes can implement this as a sort by one or more + /// of specific properties (e.g. name or bom-ref). Note that + /// handling of the "recursive" option is commonly handled in + /// the base-class method, via which these should be called. + /// Being a static method, those in derived classes are not + /// overrides for the PoV of the language. + /// + /// Ordering proposed in these methods is an educated guess. + /// Main purpose for this is to have some consistently ordered + /// serialized BomEntity lists for the purposes of comparison + /// and compression. + /// + /// TODO: this should really be offloaded as lambdas into the + /// BomEntity-derived classes themselves, but I've struggled + /// to cast the right magic spells at C# to please its gods. + /// In particular, the ValueTuple used in selector signature is + /// both generic for the values' types (e.g. ), + /// and for their amount in the tuple (0, 1, 2, ... explicitly + /// stated). So this is the next best thing... + /// + public static void NormalizeList(bool ascending, bool recursive, List list) + { + if (list is null || list.Count < 2) + { + // No-op quickly for null, empty or single-item lists + return; + } + + Type thisType = list[0].GetType(); + + if (recursive) + { + // Look into properties of each currently listed BomEntity-derived + // type instance, so if there are further lists - sort them similarly. + PropertyInfo[] properties = BomEntity.KnownEntityTypeProperties[thisType]; + foreach (PropertyInfo property in properties) + { + if (property.PropertyType == typeof(List) || property.PropertyType.ToString().StartsWith("System.Collections.Generic.List")) + { + // Re-use these learnings while we iterate all original + // list items regarding the specified sub-list property: + Type LType = null; + Type TType = null; + PropertyInfo propCount = null; + MethodInfo methodGetItem = null; + MethodInfo methodSort = null; + MethodInfo methodReverse = null; + MethodInfo methodNormalizeSubList = null; + bool retryMethodNormalizeSubList = true; + + foreach(var obj in list) + { + if (obj is null) + { + continue; + } + + try + { + // Use cached info where available for all the + // list and list-item types and methods involved. + + // Get the (list) property of the originally iterated + // BomEntity-derived item from our original list: + var propValObj = property.GetValue(obj); + + // Is that sub-list trivial enough to skip? + if (propValObj is null) + { + continue; + } + + if (LType == null) + { + LType = propValObj.GetType(); + } + + // Learn how to query that LType sort of lists: + if (methodGetItem == null || propCount == null || methodSort == null || methodReverse == null) + { + if (BomEntity.KnownEntityTypeLists.TryGetValue(LType, out BomEntityListReflection refInfo)) + { + propCount = refInfo.propCount; + methodGetItem = refInfo.methodGetItem; + methodSort = refInfo.methodSort; + methodReverse = refInfo.methodReverse; + } + else + { + propCount = LType.GetProperty("Count"); + methodGetItem = LType.GetMethod("get_Item"); + methodSort = LType.GetMethod("Sort"); + methodReverse = LType.GetMethod("Reverse"); + } + + if (methodGetItem == null || propCount == null || methodSort == null || methodReverse == null) + { + // is this really a LIST - it lacks a get_Item() or other methods, or a Count property + continue; + } + } + + // Is that sub-list trivial enough to skip? + int propValObjCount = (int)propCount.GetValue(propValObj, null); + if (propValObjCount < 2) + { + continue; + } + + // Type of items in that sub-list: + if (TType == null) + { + TType = methodGetItem.Invoke(propValObj, new object[] { 0 }).GetType(); + } + + // Learn how to sort the sub-list of those item types: + if (methodNormalizeSubList == null && retryMethodNormalizeSubList) + { + if (!KnownTypeNormalizeList.TryGetValue(TType, out var methodNormalizeSubListTmp)) + { + methodNormalizeSubListTmp = null; + retryMethodNormalizeSubList = false; + } + methodNormalizeSubList = methodNormalizeSubListTmp; + } + + if (methodNormalizeSubList != null) + { + // call static NormalizeList(..., List obj.propValObj) + methodNormalizeSubList.Invoke(null, new object[] {ascending, recursive, propValObj}); + } + else + { + // Default-sort a common sub-list directly (no recursion) + methodSort.Invoke(propValObj, null); + if (!ascending) + { + methodReverse.Invoke(propValObj, null); + } + } + } + catch (System.InvalidOperationException) + { + // property.GetValue(obj) failed + continue; + } + catch (System.Reflection.TargetInvocationException) + { + // property.GetValue(obj) failed + continue; + } + } + } + } + } + + if (KnownTypeNormalizeList.TryGetValue(thisType, out var methodNormalizeList)) + { + // Note we do not check for null/type of "obj" at this point + // since the derived classes define the logic of equivalence + // (possibly to other entity subtypes as well). + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + return; + } + + // Expensive but reliable default implementation (modulo differently + // sorted lists of identical item sets inside the otherwise identical + // objects -- but currently spec seems to mean ordered collections); + // classes are welcome to implement theirs eventually or switch cases + // above currently. + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, list, + o => (o?.SerializeEntity()), + null); + } + /// /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false From 9bd6c0296aece38bafd5f9b2faaae3df0d1850bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 20:05:29 +0200 Subject: [PATCH 203/285] ListMergeHelper.SortByImpl(): support calling BomEntity.NormalizeList() and pass the "recurse" option to it and back from it where applicable (for having the actual recursion) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 72 ++++++++++++++++++++++++-- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index bc639c7c..326f1a68 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -130,33 +130,95 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } // Adapted from https://stackoverflow.com/a/76523292/4715872 + public void SortByAscending(List list) + { + SortByImpl(true, false, list, null, null); + } + + public void SortByAscending(List list, bool recursive) + { + SortByImpl(true, recursive, list, null, null); + } + public void SortByAscending(List list, Func selector) { - SortByImpl(true, list, selector, null); + SortByImpl(true, false, list, selector, null); } public void SortByAscending(List list, Func selector, IComparer comparer) { - SortByImpl(true, list, selector, comparer); + SortByImpl(true, false, list, selector, comparer); + } + + public void SortByDescending(List list) + { + SortByImpl(false, false, list, null, null); + } + + public void SortByDescending(List list, bool recursive) + { + SortByImpl(false, recursive, list, null, null); } public void SortByDescending(List list, Func selector) { - SortByImpl(false, list, selector, null); + SortByImpl(false, false, list, selector, null); } public void SortByDescending(List list, Func selector, IComparer comparer) { - SortByImpl(false, list, selector, comparer); + SortByImpl(false, false, list, selector, comparer); } - public void SortByImpl(bool ascending, List list, Func selector, IComparer comparer) + /// + /// Implementation of the sort algorithm. + /// Special handling for BomEntity-derived objects, including + /// optional recursion to have them sort their list-of-something + /// properties. + /// + /// ValueTuple of function parameters returned by selector lambda + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + /// lambda to select a tuple of properties to sort by + /// null for default, or a custom comparer + public void SortByImpl(bool ascending, bool recursive, List list, Func selector, IComparer comparer) { if (list is null || list.Count < 2) { // No-op quickly for null, empty or single-item lists return; } + + // Ordering proposed in those NormalizeList() implementations + // is an educated guess. Main purpose for this is to have + // consistently ordered serialized BomEntity-derived type + // lists for the purposes of comparison and compression. + if (selector is null && typeof(BomEntity).IsInstanceOfType(list[0])) + { + // This should really be offloaded as lambdas into the + // BomEntity-derived classes themselves, but I've struggled + // to cast the right magic spells at C# to please its gods. + // In particular, the ValueTuple used in selector signature is + // both generic for the values' types (e.g. ), + // and for their amount in the tuple (0, 1, 2, ... explicitly + // stated). So this is the next best thing... + + // Alas, C# won't let us just call + // BomEntity.NormalizeList(ascending, recursive, (List)list) or + // something as simple, so here it goes - some more reflection: + var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + + if (methodNormalizeList != null) + { + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + } // else keep it as was? no good cause for an exception?.. + + return; + } + if (comparer is null) { comparer = Comparer.Default; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 97632729..aadfd926 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -918,7 +918,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // classes are welcome to implement theirs eventually or switch cases // above currently. var sortHelper = new ListMergeHelper(); - sortHelper.SortByImpl(ascending, list, + sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); } From 210130843977c2c1c2323a9a6f7027ce5fc0f72f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:50:17 +0200 Subject: [PATCH 204/285] ListMergeHelper: hack around type-less SortByAscending()/SortByDescending() variants Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 326f1a68..e7c896da 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -130,14 +130,14 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat } // Adapted from https://stackoverflow.com/a/76523292/4715872 - public void SortByAscending(List list) + public void SortByAscending(List list) { - SortByImpl(true, false, list, null, null); + SortByImpl(true, false, list, null, null); } - public void SortByAscending(List list, bool recursive) + public void SortByAscending(List list, bool recursive) { - SortByImpl(true, recursive, list, null, null); + SortByImpl(true, recursive, list, null, null); } public void SortByAscending(List list, Func selector) @@ -150,14 +150,14 @@ public void SortByAscending(List list, Func selector, ICompare SortByImpl(true, false, list, selector, comparer); } - public void SortByDescending(List list) + public void SortByDescending(List list) { - SortByImpl(false, false, list, null, null); + SortByImpl(false, false, list, null, null); } - public void SortByDescending(List list, bool recursive) + public void SortByDescending(List list, bool recursive) { - SortByImpl(false, recursive, list, null, null); + SortByImpl(false, recursive, list, null, null); } public void SortByDescending(List list, Func selector) From c1cd826a7395d09dc175f01b7bce5d65bc917902 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:52:29 +0200 Subject: [PATCH 205/285] BomEntity: introduce interface IBomEntity to fiddle with inheritance UPDATE: Did not help make List easier. The List<> type does not really care about data types inside, just rejects them whichever way. So reflection stays... Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index aadfd926..e933e90d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -341,6 +341,17 @@ public class BomEntityListMergeHelperReflection public Object helperInstance { get; set; } } + /// + /// Primarily used to make life with List a bit + /// easier than List when used against derived types. + /// + public interface IBomEntity + { + bool Equivalent(BomEntity obj); + bool MergeWith(BomEntity obj); + string SerializeEntity(); + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -350,7 +361,7 @@ public class BomEntityListMergeHelperReflection /// and to define the logic for merge-ability of such objects while coding much /// of the logical scaffolding only once. /// - public class BomEntity : IEquatable + public class BomEntity : IEquatable, IBomEntity { // Keep this info initialized once to cut down on overheads of reflection // when running in our run-time loops. @@ -396,7 +407,9 @@ public class BomEntity : IEquatable new Func>(() => { Dictionary dict = new Dictionary(); - foreach (var type in KnownEntityTypes) + List KnownEntityTypesPlus = new List(KnownEntityTypes); + KnownEntityTypesPlus.Add(typeof(IBomEntity)); + foreach (var type in KnownEntityTypesPlus) { // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: @@ -654,7 +667,7 @@ protected BomEntity() /// Calls our standard CycloneDX.Json.Serializer to use /// its common options in particular. /// - internal string SerializeEntity() + public string SerializeEntity() { // Do we have a custom serializer defined? Use it! // (One for BomEntity tends to serialize this base class @@ -767,7 +780,7 @@ public bool Equivalent(BomEntity obj) /// and for their amount in the tuple (0, 1, 2, ... explicitly /// stated). So this is the next best thing... /// - public static void NormalizeList(bool ascending, bool recursive, List list) + public static void NormalizeList(bool ascending, bool recursive, List list) { if (list is null || list.Count < 2) { @@ -917,7 +930,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // objects -- but currently spec seems to mean ordered collections); // classes are welcome to implement theirs eventually or switch cases // above currently. - var sortHelper = new ListMergeHelper(); + var sortHelper = new ListMergeHelper(); sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); From 1550c27e246b596a87cbe104f0826139bb12801b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:55:08 +0200 Subject: [PATCH 206/285] BomEntity: extend KnownTypeNormalizeList detection to cover IBomEntity among fallbacks, and a more specific list type for derived classes Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e933e90d..b870fedf 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -646,7 +646,41 @@ public class BomEntity : IEquatable, IBomEntity Dictionary dict = new Dictionary(); foreach (var type in KnownEntityTypes) { - var method = type.GetMethod("NormalizeList", + MethodInfo method = null; + + if (BomEntity.KnownEntityTypeLists.TryGetValue(type, out BomEntityListReflection refInfoListType)) + { + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), refInfoListType.genericType }); + if (method != null) + { + dict[type] = method; + continue; + } + } + + // Try interface default + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(IList) }); + if (method != null) + { + dict[type] = method; + continue; + } + + method = type.GetMethod("NormalizeList", + BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, + new [] { typeof(bool), typeof(bool), typeof(List) }); + if (method != null) + { + dict[type] = method; + continue; + } + + // Try class default + method = type.GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, new [] { typeof(bool), typeof(bool), typeof(List) }); if (method != null) From e0555bf15ca4704f5d6375f2371eb3360b80ed93 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 22:56:51 +0200 Subject: [PATCH 207/285] BomEntity and ListMergeHelper: fix calling of NormalizeList() and back to SortByImpl() ...if having to create copies of lists as a fake cast for List and back counts as a "fix". But hey, it works! Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 77 +++++++++++++++++++++++++- src/CycloneDX.Core/Models/BomEntity.cs | 33 +++++++++-- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index e7c896da..2c5c2790 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -204,16 +204,89 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // and for their amount in the tuple (0, 1, 2, ... explicitly // stated). So this is the next best thing... +/* + switch (typeof(TKey)) { + case Tool: + SortByImpl(ascending, list, + o => (o?.Vendor, o?.Name, o?.Version), + comparer); + return; + + case typeof(Component): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), + comparer); + return; + + case typeof(Service): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Group, o?.Name, o?.Version), + comparer); + return; + + case typeof(ExternalReference): + SortByImpl(ascending, list, + o => (o?.Url, o?.Type), + comparer); + return; + + case typeof(Dependency): + SortByImpl(ascending, list, + o => (o?.Ref), + comparer); + return; + + case typeof(Composition): + SortByImpl(ascending, list, + o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), + comparer); + return; + + case typeof(Vulnerability): + SortByImpl(ascending, list, + o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), + comparer); + return; + + default: + // Expensive but reliable (modulo differently sorted lists + // of identical item sets inside the otherwise identical + // objects); classes are welcome to implement theirs eventually + // or switch cases above currently. + SortByImplBomEntity(ascending, list, + o => (o?.SerializeEntity()), + comparer); + return; + } +*/ + // Alas, C# won't let us just call // BomEntity.NormalizeList(ascending, recursive, (List)list) or // something as simple, so here it goes - some more reflection: var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); + new [] { typeof(bool), typeof(bool), typeof(List) }); if (methodNormalizeList != null) { - methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}); + if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(IBomEntity), out BomEntityListReflection refInfoListInterface)) + { + if (BomEntity.KnownEntityTypeLists.TryGetValue(list[0].GetType(), out BomEntityListReflection refInfoListType)) + { + // Gotta make ugly cast copies there and back: + List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); + refInfoListInterface.methodAddRange.Invoke(helper, new object[] {list}); + + methodNormalizeList.Invoke(null, new object[] {ascending, recursive, helper}); + + // Populate back the original list object: + list.Clear(); + foreach (var item in helper) + { + refInfoListType.methodAdd.Invoke(list, new object[] {item}); + } + } + } } // else keep it as was? no good cause for an exception?.. return; diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index b870fedf..45587f50 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -812,7 +812,9 @@ public bool Equivalent(BomEntity obj) /// In particular, the ValueTuple used in selector signature is /// both generic for the values' types (e.g. ), /// and for their amount in the tuple (0, 1, 2, ... explicitly - /// stated). So this is the next best thing... + /// stated). So this is the next best thing... even if highly + /// inefficient to copy lists from one type to another as a + /// fake cast. At least it works!.. /// public static void NormalizeList(bool ascending, bool recursive, List list) { @@ -952,11 +954,30 @@ public static void NormalizeList(bool ascending, bool recursive, List Date: Tue, 22 Aug 2023 22:58:06 +0200 Subject: [PATCH 208/285] BomEntity: create simpler NormalizeList() aliases with default settings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 45587f50..e28f84c4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -992,6 +992,26 @@ public static void NormalizeList(bool ascending, bool recursive, List + /// See NormalizeList(); this variant defaults "ascending=true" + /// + /// Should this recurse into child-object properties which are sub-lists? + /// + public static void NormalizeList(bool recursive, List list) + { + NormalizeList(true, recursive, list); + } + + /// + /// + /// See NormalizeList(); this variant defaults "ascending=true" + /// and "recursive=false" to only normalize the given list itself. + /// + /// + public static void NormalizeList(List list) + { + NormalizeList(true, false, list); + } + /// Default implementation just "agrees" that Equals()==true objects /// are already merged (returns true), and that Equivalent()==false /// objects are not (returns false), and for others (equivalent but From 0a101e96ecbdf7d532fb189e0d80afc51d6d157d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 23:00:02 +0200 Subject: [PATCH 209/285] Merge: simplify CleanupSortLists() by not keeping sort-filters in the method - move them to class definitions and use new BomEntity.NormalizeList() codepath Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 56 ------------------- src/CycloneDX.Core/Models/Component.cs | 17 ++++++ src/CycloneDX.Core/Models/Composition.cs | 19 ++++++- src/CycloneDX.Core/Models/Dependency.cs | 19 ++++++- .../Models/ExternalReference.cs | 17 ++++++ src/CycloneDX.Core/Models/Service.cs | 17 ++++++ src/CycloneDX.Core/Models/Tool.cs | 17 ++++++ .../Models/Vulnerabilities/Vulnerability.cs | 17 ++++++ src/CycloneDX.Utils/Merge.cs | 21 ++++--- 9 files changed, 135 insertions(+), 65 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index 2c5c2790..fa8ec8c4 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -204,62 +204,6 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // and for their amount in the tuple (0, 1, 2, ... explicitly // stated). So this is the next best thing... -/* - switch (typeof(TKey)) { - case Tool: - SortByImpl(ascending, list, - o => (o?.Vendor, o?.Name, o?.Version), - comparer); - return; - - case typeof(Component): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), - comparer); - return; - - case typeof(Service): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Group, o?.Name, o?.Version), - comparer); - return; - - case typeof(ExternalReference): - SortByImpl(ascending, list, - o => (o?.Url, o?.Type), - comparer); - return; - - case typeof(Dependency): - SortByImpl(ascending, list, - o => (o?.Ref), - comparer); - return; - - case typeof(Composition): - SortByImpl(ascending, list, - o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), - comparer); - return; - - case typeof(Vulnerability): - SortByImpl(ascending, list, - o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), - comparer); - return; - - default: - // Expensive but reliable (modulo differently sorted lists - // of identical item sets inside the otherwise identical - // objects); classes are welcome to implement theirs eventually - // or switch cases above currently. - SortByImplBomEntity(ascending, list, - o => (o?.SerializeEntity()), - comparer); - return; - } -*/ - // Alas, C# won't let us just call // BomEntity.NormalizeList(ascending, recursive, (List)list) or // something as simple, so here it goes - some more reflection: diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 955a68dc..0d36298f 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -238,6 +238,23 @@ public bool Equivalent(Component obj) return false; } + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version), + null); + } + public bool MergeWith(Component obj) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index c6643738..c0081b1e 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -141,5 +141,22 @@ public void WriteXml(System.Xml.XmlWriter writer) { writer.WriteEndElement(); } } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Aggregate, o?.Assemblies, o?.Dependencies), + null); + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 70a2e948..15471381 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -35,5 +35,22 @@ public class Dependency : BomEntity [XmlElement("dependency")] [ProtoMember(2)] public List Dependencies { get; set; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Ref), + null); + } } -} \ No newline at end of file +} diff --git a/src/CycloneDX.Core/Models/ExternalReference.cs b/src/CycloneDX.Core/Models/ExternalReference.cs index 84d12e5d..421ea511 100644 --- a/src/CycloneDX.Core/Models/ExternalReference.cs +++ b/src/CycloneDX.Core/Models/ExternalReference.cs @@ -79,5 +79,22 @@ public enum ExternalReferenceType [ProtoMember(4)] public List Hashes { get; set; } public bool ShouldSerializeHashes() { return Hashes?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Url, o?.Type), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 9de775b3..ef47f347 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -123,5 +123,22 @@ public bool NonNullableXTrustBoundary [XmlArrayItem("property")] [ProtoMember(14)] public List Properties { get; set; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Group, o?.Name, o?.Version), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Tool.cs b/src/CycloneDX.Core/Models/Tool.cs index 6674462c..fdb9fcaa 100644 --- a/src/CycloneDX.Core/Models/Tool.cs +++ b/src/CycloneDX.Core/Models/Tool.cs @@ -46,5 +46,22 @@ public class Tool : BomEntity [ProtoMember(5)] public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() { return ExternalReferences?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.Vendor, o?.Name, o?.Version), + null); + } } } diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index 372e8247..2bbe624e 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -124,5 +124,22 @@ public DateTime? Updated [ProtoMember(18)] public List Properties { get; set; } public bool ShouldSerializeProperties() { return Properties?.Count > 0; } + + /// + /// See BomEntity.NormalizeList() and ListMergeHelper.SortByImpl(). + /// Note that as a static method this is not exactly an "override", + /// but the BomEntity base class implementation makes it behave + /// like that in practice. + /// + /// Ascending (true) or Descending (false) + /// Passed to BomEntity.NormalizeList() (effective if recursing), not handled right here + /// List to sort + public static void NormalizeList(bool ascending, bool recursive, List list) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByImpl(ascending, recursive, list, + o => (o?.BomRef, o?.Id, o?.Created, o?.Updated), + null); + } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 863af6a5..9c760f75 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -535,46 +535,53 @@ public static Bom CleanupEmptyLists(Bom result) /// Resulting document (whether modified or not) public static Bom CleanupSortLists(Bom result) { + // Why oh why?.. error CS1503: Argument 1: cannot convert + // from 'System.Collections.Generic.List' + // to 'System.Collections.Generic.List' + // BomEntity.NormalizeList(result.Tools) -- it looks so simple! + // But at least we *can* call it, perhaps inefficiently for + // the run-time code and scaffolding, but easy to maintain + // with filter definitions now stored in classes, not here... if (result.Metadata?.Tools?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Metadata.Tools, o => (o?.Vendor, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Metadata.Tools); } if (result.Components?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Components, o => (o?.BomRef, o?.Type, o?.Group, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Components); } if (result.Services?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Services, o => (o?.BomRef, o?.Group, o?.Name, o?.Version)); + sortHelper.SortByAscending(result.Services); } if (result.ExternalReferences?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.ExternalReferences, o => (o?.Url, o?.Type)); + sortHelper.SortByAscending(result.ExternalReferences); } if (result.Dependencies?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Dependencies, o => (o?.Ref)); + sortHelper.SortByAscending(result.Dependencies); } if (result.Compositions?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Compositions, o => (o?.Aggregate, o?.Assemblies, o?.Dependencies)); + sortHelper.SortByAscending(result.Compositions); } if (result.Vulnerabilities?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Vulnerabilities, o => (o?.BomRef, o?.Id, o?.Created, o?.Updated)); + sortHelper.SortByAscending(result.Vulnerabilities); } return result; From 283544bb4e8c239bdc2a482caecaac36b65bc0b5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 22 Aug 2023 23:04:35 +0200 Subject: [PATCH 210/285] Drop IBomEntity as a failed and useless experiment Signed-off-by: Jim Klimov --- src/CycloneDX.Core/ListMergeHelper.cs | 6 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 46 ++++---------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/CycloneDX.Core/ListMergeHelper.cs b/src/CycloneDX.Core/ListMergeHelper.cs index fa8ec8c4..28920639 100644 --- a/src/CycloneDX.Core/ListMergeHelper.cs +++ b/src/CycloneDX.Core/ListMergeHelper.cs @@ -209,16 +209,16 @@ public void SortByImpl(bool ascending, bool recursive, List list, Func< // something as simple, so here it goes - some more reflection: var methodNormalizeList = typeof(BomEntity).GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); + new [] { typeof(bool), typeof(bool), typeof(List) }); if (methodNormalizeList != null) { - if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(IBomEntity), out BomEntityListReflection refInfoListInterface)) + if (BomEntity.KnownEntityTypeLists.TryGetValue(typeof(BomEntity), out BomEntityListReflection refInfoListInterface)) { if (BomEntity.KnownEntityTypeLists.TryGetValue(list[0].GetType(), out BomEntityListReflection refInfoListType)) { // Gotta make ugly cast copies there and back: - List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); + List helper = (List)Activator.CreateInstance(refInfoListInterface.genericType); refInfoListInterface.methodAddRange.Invoke(helper, new object[] {list}); methodNormalizeList.Invoke(null, new object[] {ascending, recursive, helper}); diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e28f84c4..b0294074 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -341,17 +341,6 @@ public class BomEntityListMergeHelperReflection public Object helperInstance { get; set; } } - /// - /// Primarily used to make life with List a bit - /// easier than List when used against derived types. - /// - public interface IBomEntity - { - bool Equivalent(BomEntity obj); - bool MergeWith(BomEntity obj); - string SerializeEntity(); - } - /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -361,7 +350,7 @@ public interface IBomEntity /// and to define the logic for merge-ability of such objects while coding much /// of the logical scaffolding only once. /// - public class BomEntity : IEquatable, IBomEntity + public class BomEntity : IEquatable { // Keep this info initialized once to cut down on overheads of reflection // when running in our run-time loops. @@ -407,9 +396,7 @@ public class BomEntity : IEquatable, IBomEntity new Func>(() => { Dictionary dict = new Dictionary(); - List KnownEntityTypesPlus = new List(KnownEntityTypes); - KnownEntityTypesPlus.Add(typeof(IBomEntity)); - foreach (var type in KnownEntityTypesPlus) + foreach (var type in KnownEntityTypes) { // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: @@ -660,25 +647,6 @@ public class BomEntity : IEquatable, IBomEntity } } - // Try interface default - method = type.GetMethod("NormalizeList", - BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(IList) }); - if (method != null) - { - dict[type] = method; - continue; - } - - method = type.GetMethod("NormalizeList", - BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, - new [] { typeof(bool), typeof(bool), typeof(List) }); - if (method != null) - { - dict[type] = method; - continue; - } - // Try class default method = type.GetMethod("NormalizeList", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, @@ -816,7 +784,7 @@ public bool Equivalent(BomEntity obj) /// inefficient to copy lists from one type to another as a /// fake cast. At least it works!.. /// - public static void NormalizeList(bool ascending, bool recursive, List list) + public static void NormalizeList(bool ascending, bool recursive, List list) { if (list is null || list.Count < 2) { @@ -956,7 +924,7 @@ public static void NormalizeList(bool ascending, bool recursive, List(); + var sortHelper = new ListMergeHelper(); sortHelper.SortByImpl(ascending, recursive, list, o => (o?.SerializeEntity()), null); @@ -996,7 +964,7 @@ public static void NormalizeList(bool ascending, bool recursive, List /// Should this recurse into child-object properties which are sub-lists? /// - public static void NormalizeList(bool recursive, List list) + public static void NormalizeList(bool recursive, List list) { NormalizeList(true, recursive, list); } @@ -1007,7 +975,7 @@ public static void NormalizeList(bool recursive, List list) /// and "recursive=false" to only normalize the given list itself. /// /// - public static void NormalizeList(List list) + public static void NormalizeList(List list) { NormalizeList(true, false, list); } From 06343c653758abd7cf614de0dc145f1fbd4f7e5e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 23 Aug 2023 01:49:42 +0200 Subject: [PATCH 211/285] BomMerge.cs: fix codacy (bogus) warnings Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index b0294074..8acff35f 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -908,12 +908,10 @@ public static void NormalizeList(bool ascending, bool recursive, List catch (System.InvalidOperationException) { // property.GetValue(obj) failed - continue; } catch (System.Reflection.TargetInvocationException) { // property.GetValue(obj) failed - continue; } } } @@ -929,7 +927,8 @@ public static void NormalizeList(bool ascending, bool recursive, List // Note we do not check for null/type of "obj" at this point // since the derived classes define the logic of equivalence // (possibly to other entity subtypes as well). - //methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}) does not work, alas + // methodNormalizeList.Invoke(null, new object[] {ascending, recursive, list}) does + // not work, alas // Gotta make ugly cast copies there and back: var helper = Activator.CreateInstance(refInfoListType.genericType); @@ -942,7 +941,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // Populate back the original list object: list.Clear(); - refInfoListInterface.methodAddRange.Invoke(list, new object[] {helper}); + refInfoListInterface.methodAddRange.Invoke(list, new [] {helper}); return; } } @@ -950,7 +949,7 @@ public static void NormalizeList(bool ascending, bool recursive, List // Expensive but reliable default implementation (modulo differently // sorted lists of identical item sets inside the otherwise identical - // objects -- but currently spec seems to mean ordered collections); + // objects -- but currently spec seems to mean ordered collections), // classes are welcome to implement theirs eventually or switch cases // above currently. var sortHelper = new ListMergeHelper(); From 0641912e184b6a02b35252506beac58510d45314 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 23 Aug 2023 02:14:43 +0200 Subject: [PATCH 212/285] BomEntity: KnownEntityTypeLists[]: track BomEntity.NormalizeList() too Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 8acff35f..0cd74524 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -396,7 +396,9 @@ public class BomEntity : IEquatable new Func>(() => { Dictionary dict = new Dictionary(); - foreach (var type in KnownEntityTypes) + List KnownEntityTypesPlus = new List(KnownEntityTypes); + KnownEntityTypesPlus.Add(typeof(BomEntity)); + foreach (var type in KnownEntityTypesPlus) { // Inspired by https://stackoverflow.com/a/4661237/4715872 // to craft a List "result" at run-time: From c2c2e6f45c196a4659899a1ee36a5242f28ef86d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 23 Aug 2023 02:15:42 +0200 Subject: [PATCH 213/285] Merge: CleanupSortLists(): make use of recursive sorting of BomEntity objects Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 9c760f75..5e4ecc66 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -545,43 +545,43 @@ public static Bom CleanupSortLists(Bom result) if (result.Metadata?.Tools?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Metadata.Tools); + sortHelper.SortByAscending(result.Metadata.Tools, true); } if (result.Components?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Components); + sortHelper.SortByAscending(result.Components, true); } if (result.Services?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Services); + sortHelper.SortByAscending(result.Services, true); } if (result.ExternalReferences?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.ExternalReferences); + sortHelper.SortByAscending(result.ExternalReferences, true); } if (result.Dependencies?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Dependencies); + sortHelper.SortByAscending(result.Dependencies, true); } if (result.Compositions?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Compositions); + sortHelper.SortByAscending(result.Compositions, true); } if (result.Vulnerabilities?.Count > 0) { var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Vulnerabilities); + sortHelper.SortByAscending(result.Vulnerabilities, true); } return result; From 0449e94772f8be653082ddb1e45f0e167787a072 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 29 Aug 2023 14:47:31 +0200 Subject: [PATCH 214/285] BomEntity and derived types: extend MergeWith() API signature to handle a BomEntityListMergeHelperStrategy argument Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 14 ++++++++------ src/CycloneDX.Core/Models/Component.cs | 15 +++++++++------ src/CycloneDX.Core/Models/Hash.cs | 7 +++++-- src/CycloneDX.Utils/Merge.cs | 5 +++-- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0cd74524..383f3fcb 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -229,11 +229,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat var item1 = result[i]; if (methodMergeWith != null) { - resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0}); + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item0, listMergeHelperStrategy}); } else { - resMerge = item1.MergeWith(item0); + resMerge = item1.MergeWith(item0, listMergeHelperStrategy); } if (resMerge) @@ -281,11 +281,11 @@ public List Merge(List list1, List list2, BomEntityListMergeHelperStrat bool resMerge; if (methodMergeWith != null) { - resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2}); + resMerge = (bool)methodMergeWith.Invoke(item1, new object[] {item2, listMergeHelperStrategy}); } else { - resMerge = item1.MergeWith(item2); + resMerge = item1.MergeWith(item2, listMergeHelperStrategy); } // MergeWith() may throw BomEntityConflictException which we // want to propagate to users - their input data is confusing. @@ -614,7 +614,7 @@ public class BomEntity : IEquatable { var method = type.GetMethod("MergeWith", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - new [] { type }); + new [] { type, typeof(BomEntityListMergeHelperStrategy) }); if (method != null) { dict[type] = method; @@ -990,13 +990,15 @@ public static void NormalizeList(List list) /// /// Another object of same type whose additional /// non-conflicting data we try to squash into this object. + /// A BomEntityListMergeHelperStrategy + /// instance which relays nuances about desired merging activity. /// True if merge was successful, False if it these objects /// are not equivalent, or throws if merge can not be done (including /// lack of merge logic or unresolvable conflicts in data points). /// /// Source data problem: two entities with conflicting information /// Caller error: somehow merging different entity types - public bool MergeWith(BomEntity obj) + public bool MergeWith(BomEntity obj, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (obj is null) { diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0d36298f..cefc481d 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -255,7 +255,10 @@ public static void NormalizeList(bool ascending, bool recursive, List null); } - public bool MergeWith(Component obj) + /// + /// See BomEntity.MergeWith() + /// + public bool MergeWith(Component obj, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) { @@ -267,7 +270,7 @@ public bool MergeWith(Component obj) // Basic checks for null, type compatibility, // equality and non-equivalence; throws for // the hard stuff to implement in the catch: - bool resBase = base.MergeWith(obj); + bool resBase = base.MergeWith(obj, listMergeHelperStrategy); if (iDebugLevel >= 1) { if (resBase) @@ -548,7 +551,7 @@ public bool MergeWith(Component obj) if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType, typeof(BomEntityListMergeHelperStrategy) }) methodMergeWith = null; } @@ -628,7 +631,7 @@ public bool MergeWith(Component obj) { Console.WriteLine($"Component.MergeWith(): Call futher {TType.ToString()}.mergeWith() for '{property.Name}': merge of {tmpItem?.ToString()} and {objItem?.ToString()}"); } - if (!((bool)methodMergeWith.Invoke(tmpItem, new [] {objItem}))) + if (!((bool)methodMergeWith.Invoke(tmpItem, new [] {objItem, listMergeHelperStrategy}))) { mergedOk = false; } @@ -809,7 +812,7 @@ public bool MergeWith(Component obj) if (!KnownTypeMergeWith.TryGetValue(TType, out var methodMergeWith)) { // No need to re-query now that we have BomEntity descendance: - // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType }) + // e.g. methodMergeWith = TType.GetMethod("MergeWith", 0, new [] { TType, typeof(BomEntityListMergeHelperStrategy) }) methodMergeWith = null; } @@ -817,7 +820,7 @@ public bool MergeWith(Component obj) { try { - if (!((bool)methodMergeWith.Invoke(propValTmp, new [] {propValObj}))) + if (!((bool)methodMergeWith.Invoke(propValTmp, new [] {propValObj, listMergeHelperStrategy}))) { mergedOk = false; } diff --git a/src/CycloneDX.Core/Models/Hash.cs b/src/CycloneDX.Core/Models/Hash.cs index 1bf4d761..253442b9 100644 --- a/src/CycloneDX.Core/Models/Hash.cs +++ b/src/CycloneDX.Core/Models/Hash.cs @@ -68,14 +68,17 @@ public bool Equivalent(Hash obj) return (!(obj is null) && this.Alg == obj.Alg); } - public bool MergeWith(Hash obj) + /// + /// See BomEntity.MergeWith() + /// + public bool MergeWith(Hash obj, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { try { // Basic checks for null, type compatibility, // equality and non-equivalence; throws for // the hard stuff to implement in the catch: - return base.MergeWith(obj); + return base.MergeWith(obj, listMergeHelperStrategy); } catch (BomEntityConflictException) { diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 5e4ecc66..10250f02 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -112,7 +112,7 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); } result.Metadata.Component = bom1.Metadata.Component; - result.Metadata.Component.MergeWith(bom2.Metadata.Component); + result.Metadata.Component.MergeWith(bom2.Metadata.Component, listMergeHelperStrategy); } else { @@ -446,6 +446,7 @@ public static Bom CleanupMetadataComponent(Bom result) if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) { + BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); if (iDebugLevel >= 2) { Console.WriteLine($"MERGE-CLEANUP: Searching in list"); @@ -467,7 +468,7 @@ public static Bom CleanupMetadataComponent(Bom result) { Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); } - result.Metadata.Component.MergeWith(component); + result.Metadata.Component.MergeWith(component, safeStrategy); result.Components.Remove(component); return result; } From c5af163d0e1a3052ea2bf241db9169cd8e33c63e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 29 Aug 2023 14:47:55 +0200 Subject: [PATCH 215/285] BomEntityListMergeHelperStrategy: add and explain the renameConflictingComponents option Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 383f3fcb..595d52ad 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -82,6 +82,64 @@ public class BomEntityListMergeHelperStrategy /// public bool useBomEntityMerge { get; set; } + /// + /// When merging whole Bom documents which include + /// Equivalent() Components (and probably references + /// back to them in respective Dependencies[] lists) + /// with differing values of Scope (required or null + /// vs. optional vs. excluded), do not conflate them + /// but instead rename the two siblings' values of + /// "bom-ref", suffixing the ":scope" - including + /// the back-references from locations known by spec. + /// Also consider equality of non-null Dependencies + /// pointing back to their same BomRef value in the + /// two original Bom documents (notably honouring the + /// explicitly empty "dependsOn" lists -- NOT NULL). + /// + /// This is partially orthogonal to useBomEntityMerge + /// setting which would allow to populate missing + /// data points using an incoming Component object: + /// * "partially" being that when two Components would + /// be inspected by MergeWith(), the possibiliy of + /// such suffix would be considered among equality + /// criteria (not exact equality of BomRef props). + /// * "orthogonal" relating to the fact that this conflict + /// inspection aims to be a quick pre-processing stage + /// similar to quick merge (useBomEntityMerge==false) + /// and modifies the incoming list of Bom documents + /// before that quick merge, with a targeted solution + /// cheaper than a full MergeWith() iteration. + /// + /// This is a bit costlier in processing, but safer in + /// pedantic approach, than the known alternatives: + /// * Just following "useBomEntityMerge" to the letter, + /// comparing for exact equality of serialization of + /// the two objects -- two or more copies of the same + /// BomRef value assigned to different but related + /// "real-life" entities can appear (e.g. when "scope" + /// differs, like for production and testing modules) + /// AND different Dependencies[] entries can exist + /// (e.g. different Maven resolutions when building + /// a Java ecosystem library vs. an app using it, + /// with different dependencyManagement preferences). + /// Due to this, we can not quickly conflate "purely + /// equal" entities as the first pass when such + /// nuanced inequalities can arise. + /// * Brutely conflating the Components with different + /// Scopes ("optional" becomes "required" if something + /// else in the overall merged product did require it) + /// can backfire if the merged document describes an + /// end-user bundle of a number of products: their + /// separate programs (or even containers) do still have + /// their separate dependency trees, so "app A" requiring + /// a library does not mean that "app B" which had it as + /// optional suddenly requires it now -- and maybe gets + /// false-positive vulnerabilities reported due to that. + /// For merged Bom documents describing a single linker + /// namespace such conflation may in fact be valid however. + /// + public bool renameConflictingComponents { get; set; } + /// /// CycloneDX spec version. /// @@ -97,6 +155,7 @@ public static BomEntityListMergeHelperStrategy Default() return new BomEntityListMergeHelperStrategy { useBomEntityMerge = true, + renameConflictingComponents = true, specificationVersion = SpecificationVersionHelpers.CurrentVersion }; } From 90951dd043c48083ac05a4faf3fd445d0b93442f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 29 Aug 2023 16:34:28 +0200 Subject: [PATCH 216/285] Merge.cs: refactor with ReferThisToolkitMetadata() and update some comments Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 126 ++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 10250f02..d277635d 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -57,6 +57,21 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) return FlatMerge(bom1, bom2, BomEntityListMergeHelperStrategy.Default()); } + /// + /// Handle merging of two Bom object contents, possibly de-duplicating + /// or merging information from Equivalent() entries as further tuned + /// via listMergeHelperStrategy argument. + /// + /// NOTE: This sets a new timestamp into each newly merged Bom document. + /// However it is up to the caller to use ReferThisToolkitMetadata() + /// for adding references to this library (and the run-time program + /// which consumes it) into the final merged document, to avoid the + /// overhead in a loop context. + /// + /// + /// + /// + /// public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) { if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) @@ -72,6 +87,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy // document gets a new timestamp every time. It is unique after all. // Also note that a merge of "new Bom()" with a real Bom is also different // from that original (serialNumber, timestamp, possible entry order, etc.) + // Adding Tools[] entries to refer to this library (and the run-time tool + // program which consumes it) costs a bit more, so this is only done by the + // caller via ReferThisToolkitMetadata() for final merge and not in a loop. Timestamp = DateTime.Now }; @@ -211,44 +229,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. - var resultSubj = new Bom(); - - // Add reference to this currently running build of cyclonedx-cli - // (likely) and this cyclonedx-dotnet-library into the metadata/tools - // of the merged BOM document. After all - any bugs appearing due - // to merge routines are our own and should be trackable... - // Per https://stackoverflow.com/a/36351902/4715872 : - // Use System.Reflection.Assembly.GetExecutingAssembly() - // to get the assembly (that this line of code is in), or - // use System.Reflection.Assembly.GetEntryAssembly() to - // get the assembly your project started with (most likely - // this is your app). In multi-project solutions this is - // something to keep in mind! - Tool toolThisLibrary = new Tool - { - Vendor = "OWASP Foundation", - Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" - Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() - }; - - resultSubj.Metadata = new Metadata - { - Tools = new List(new [] {toolThisLibrary}) - }; - - // At worst, these would dedup away?.. - string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar - if (toolThisScriptName != toolThisLibrary.Name) - { - Tool toolThisScript = new Tool - { - Name = toolThisScriptName, - Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), - Version = Assembly.GetEntryAssembly().GetName().Version.ToString() - }; - resultSubj.Metadata.Tools.Add(toolThisScript); - } - + var resultSubj = ReferThisToolkitMetadata(null); if (bomSubject is null) { @@ -425,6 +406,73 @@ bom.SerialNumber is null return result; } + /// + /// Add reference to this currently running build of cyclonedx-cli + /// (likely) and this cyclonedx-dotnet-library into the metadata/tools + /// of the merged BOM document. After all - any bugs appearing due + /// to merge routines are our own and should be trackable... + /// + /// The Bom object to inject Metadata.Tools[] + /// entries into. May be null, then a new Bom will be provisioned. + /// + public static Bom ReferThisToolkitMetadata(Bom bom) + { + // Per https://stackoverflow.com/a/36351902/4715872 : + // Use System.Reflection.Assembly.GetExecutingAssembly() + // to get the assembly (that this line of code is in), or + // use System.Reflection.Assembly.GetEntryAssembly() to + // get the assembly your project started with (most likely + // this is your app). In multi-project solutions this is + // something to keep in mind! + Tool toolThisLibrary = new Tool + { + Vendor = "OWASP Foundation", + Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" + Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() + }; + + if (bom is null) + { + bom = new Bom(); + } + + if (bom.Metadata is null) + { + bom.Metadata = new Metadata(); + } + + if (bom.Metadata.Tools is null) + { + bom.Metadata.Tools = new List(new [] {toolThisLibrary}); + } + else + { + if (!bom.Metadata.Tools.Contains(toolThisLibrary)) + { + bom.Metadata.Tools.Add(toolThisLibrary); + } + } + + // At worst, these would dedup away?.. + string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar + if (toolThisScriptName != toolThisLibrary.Name) + { + Tool toolThisScript = new Tool + { + Name = toolThisScriptName, + Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Version = Assembly.GetEntryAssembly().GetName().Version.ToString() + }; + + if (!bom.Metadata.Tools.Contains(toolThisScript)) + { + bom.Metadata.Tools.Add(toolThisScript); + } + } + + return bom; + } + /// /// Merge main "metadata/component" entry with its possible alter-ego /// in the components list and evict extra copy from that list: per @@ -589,7 +637,7 @@ public static Bom CleanupSortLists(Bom result) } // Currently our MergeWith() logic has potential to mess with - // Component bom entities (later maybe more), and generally + // Component bom entities (later maybe more types), and generally // the document-wide uniqueness of BomRefs is a sore point, so // we want them all accounted "before and after" the (flat) merge. // Code below reuses the same dictionary object as initialized From ff0cff9d0eb5e5162d05f714e50e2cb3f9382d20 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 30 Aug 2023 16:31:10 +0200 Subject: [PATCH 217/285] Bom.cs: add ability to update Bom document basic metadata (Timestamp, SerialNumber, Version, Metadata/Tools[] for current library and its consumer script) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 125 ++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 393c0973..184990ed 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -138,5 +139,127 @@ public int NonNullableVersion // by several strategy implementations in CycloneDX.Utils Merge.cs // so maybe there should be sub-classes or strategy arguments or // properties to select one of those implementations at run-time?.. + + /// + /// Add reference to this currently running build of cyclonedx-cli + /// (likely) and this cyclonedx-dotnet-library into the Metadata/Tools + /// of this Bom document. Intended for use after processing which + /// creates or modifies the document. After all - any bugs appearing + /// due to library routines are our own and should be trackable... + /// + /// NOTE: Tries to not add identical duplicate entries. + /// + public void BomMetadataReferThisToolkit() + { + // Per https://stackoverflow.com/a/36351902/4715872 : + // Use System.Reflection.Assembly.GetExecutingAssembly() + // to get the assembly (that this line of code is in), or + // use System.Reflection.Assembly.GetEntryAssembly() to + // get the assembly your project started with (most likely + // this is your app). In multi-project solutions this is + // something to keep in mind! + Tool toolThisLibrary = new Tool + { + Vendor = "OWASP Foundation", + Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" + Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() + }; + + if (this.Metadata is null) + { + this.Metadata = new Metadata(); + } + + if (this.Metadata.Tools is null) + { + this.Metadata.Tools = new List(new [] {toolThisLibrary}); + } + else + { + if (!this.Metadata.Tools.Contains(toolThisLibrary)) + { + this.Metadata.Tools.Add(toolThisLibrary); + } + } + + // At worst, these would dedup away?.. + string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar + if (toolThisScriptName != toolThisLibrary.Name) + { + Tool toolThisScript = new Tool + { + Name = toolThisScriptName, + Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Version = Assembly.GetEntryAssembly().GetName().Version.ToString() + }; + + if (!this.Metadata.Tools.Contains(toolThisScript)) + { + this.Metadata.Tools.Add(toolThisScript); + } + } + } + + /// + /// Update the Metadata/Timestamp of this Bom document + /// (after content manipulations such as a merge) + /// using DateTime.Now. + /// + /// NOTE: Creates a new Metadata object to populate + /// the property, if one was missing in this Bom object. + /// + public void BomMetadataUpdateTimestamp() + { + if (this.Metadata is null) + { + this.Metadata = new Metadata(); + } + + this.Metadata.Timestamp = DateTime.Now; + } + + /// + /// Update the SerialNumber and optionally bump the Version + /// of a Bom document issued with such serial number (both + /// not in the Metadata structure, but still are "meta data") + /// of this Bom document, either using a new random UUID as + /// the SerialNumber and assigning a Version=1, or bumping + /// the Version -- usually done after content manipulations + /// such as a merge, depending on their caller-defined impact. + /// + public void BomMetadataUpdateSerialNumberVersion(bool generateNewSerialNumber) + { + if (this.Version is null || this.Version < 1 || this.SerialNumber is null || this.SerialNumber == "") + { + generateNewSerialNumber = true; + } + + if (generateNewSerialNumber) + { + this.Version = 1; + this.SerialNumber = "urn:uuid:" + System.Guid.NewGuid().ToString(); + } + else + { + this.Version++; + } + } + + /// + /// Set up (default or update) meta data of this Bom document, + /// covering the Version, SerialNumber and Metadata/Timestamp + /// in one shot. Typically useful to brush up a `new Bom()` or + /// to ensure a new identity for a modified Bom document. + /// + /// NOTE: caller may want to BomMetadataReferThisToolkit() + /// separately, to add the Metadata/Tools[] entries about this + /// CycloneDX library and its consumer (e.g. the "cyclonedx-cli" + /// program). + /// + public void BomMetadataUpdate(bool generateNewSerialNumber) + { + this.BomMetadataUpdateSerialNumberVersion(generateNewSerialNumber); + this.BomMetadataUpdateTimestamp(); + } } -} \ No newline at end of file +} From 4a0f409ba272dd42fc45b68787e56450a7cba94f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 30 Aug 2023 16:33:30 +0200 Subject: [PATCH 218/285] Merge.cs: refactor away ReferThisToolkitMetadata() in favor of Bom.BomMetadataReferThisToolkit() Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 72 ++---------------------------------- 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index d277635d..32a5822d 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -63,7 +63,7 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) /// via listMergeHelperStrategy argument. /// /// NOTE: This sets a new timestamp into each newly merged Bom document. - /// However it is up to the caller to use ReferThisToolkitMetadata() + /// However it is up to the caller to use Bom.BomMetadataReferThisToolkit() /// for adding references to this library (and the run-time program /// which consumes it) into the final merged document, to avoid the /// overhead in a loop context. @@ -229,7 +229,8 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // identical entries). Run another merge, careful this time, over // the resulting collection with a lot fewer items to inspect with // the heavier logic. - var resultSubj = ReferThisToolkitMetadata(null); + var resultSubj = new Bom(); + resultSubj.BomMetadataReferThisToolkit(); if (bomSubject is null) { @@ -406,73 +407,6 @@ bom.SerialNumber is null return result; } - /// - /// Add reference to this currently running build of cyclonedx-cli - /// (likely) and this cyclonedx-dotnet-library into the metadata/tools - /// of the merged BOM document. After all - any bugs appearing due - /// to merge routines are our own and should be trackable... - /// - /// The Bom object to inject Metadata.Tools[] - /// entries into. May be null, then a new Bom will be provisioned. - /// - public static Bom ReferThisToolkitMetadata(Bom bom) - { - // Per https://stackoverflow.com/a/36351902/4715872 : - // Use System.Reflection.Assembly.GetExecutingAssembly() - // to get the assembly (that this line of code is in), or - // use System.Reflection.Assembly.GetEntryAssembly() to - // get the assembly your project started with (most likely - // this is your app). In multi-project solutions this is - // something to keep in mind! - Tool toolThisLibrary = new Tool - { - Vendor = "OWASP Foundation", - Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" - Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() - }; - - if (bom is null) - { - bom = new Bom(); - } - - if (bom.Metadata is null) - { - bom.Metadata = new Metadata(); - } - - if (bom.Metadata.Tools is null) - { - bom.Metadata.Tools = new List(new [] {toolThisLibrary}); - } - else - { - if (!bom.Metadata.Tools.Contains(toolThisLibrary)) - { - bom.Metadata.Tools.Add(toolThisLibrary); - } - } - - // At worst, these would dedup away?.. - string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar - if (toolThisScriptName != toolThisLibrary.Name) - { - Tool toolThisScript = new Tool - { - Name = toolThisScriptName, - Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), - Version = Assembly.GetEntryAssembly().GetName().Version.ToString() - }; - - if (!bom.Metadata.Tools.Contains(toolThisScript)) - { - bom.Metadata.Tools.Add(toolThisScript); - } - } - - return bom; - } - /// /// Merge main "metadata/component" entry with its possible alter-ego /// in the components list and evict extra copy from that list: per From 316098d840087c720893385be6d4376a14cfb5d6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 30 Aug 2023 16:34:56 +0200 Subject: [PATCH 219/285] BomEntityListMergeHelperStrategy: add toggles to request calls to Bom.BomMetadataUpdate*() and Bom.BomMetadataReferThisToolkit() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 595d52ad..0e832f96 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -145,6 +145,22 @@ public class BomEntityListMergeHelperStrategy /// public SpecificationVersion specificationVersion { get; set; } + /// + /// Used by interim Merge.FlatMerge(bom1, bom2) in a loop + /// context -- defaulting to `false` to reduce compute + /// load for results we would discard. Can be set to `true` + /// by some other use-cases that would invoke that method. + /// Does not impact the Merge.FlatMerge(Iterable) variant. + /// + /// See also: doBomMetadataUpdateNewSerialNumber, + /// doBomMetadataUpdateReferThisToolkit + /// + public bool doBomMetadataUpdate { get; set; } + /// See doBomMetadataUpdate description. + public bool doBomMetadataUpdateNewSerialNumber { get; set; } + /// See doBomMetadataUpdate description. + public bool doBomMetadataUpdateReferThisToolkit { get; set; } + /// /// Return reasonable default strategy settings. /// @@ -156,6 +172,9 @@ public static BomEntityListMergeHelperStrategy Default() { useBomEntityMerge = true, renameConflictingComponents = true, + doBomMetadataUpdate = false, + doBomMetadataUpdateNewSerialNumber = false, + doBomMetadataUpdateReferThisToolkit = false, specificationVersion = SpecificationVersionHelpers.CurrentVersion }; } From 0964657a56ef9015479fd67e08f98e6ca3ee2b63 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 30 Aug 2023 16:35:32 +0200 Subject: [PATCH 220/285] Merge.cs: refactor updates of Bom Timestamp and similar basic metadata via BomEntityListMergeHelperStrategy toggles Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 42 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 32a5822d..3b9fab38 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -80,18 +80,29 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy } var result = new Bom(); - result.Metadata = new Metadata - { - // Note: we recurse into this method from other FlatMerge() implementations - // (e.g. mass-merge of a big list of Bom documents), so the resulting - // document gets a new timestamp every time. It is unique after all. - // Also note that a merge of "new Bom()" with a real Bom is also different - // from that original (serialNumber, timestamp, possible entry order, etc.) - // Adding Tools[] entries to refer to this library (and the run-time tool - // program which consumes it) costs a bit more, so this is only done by the - // caller via ReferThisToolkitMetadata() for final merge and not in a loop. - Timestamp = DateTime.Now - }; + // Note: we recurse into this method from other FlatMerge() implementations + // (e.g. mass-merge of a big list of Bom documents), so the resulting + // document gets a new timestamp every time. It is unique after all. + // Also note that a merge of "new Bom()" with a real Bom is also different + // from that original (serialNumber, timestamp, possible entry order, etc.) + // Adding Tools[] entries to refer to this library (and the run-time tool + // program which consumes it) costs a bit more, so this is toggled separately + // and should not waste CPU not in a loop. + // Note that these toggles default to `false` so should not impact the + // typical loop (calls from the other FlatMerge() implementations nearby). + if (listMergeHelperStrategy.doBomMetadataUpdate) + { + result.BomMetadataUpdate(listMergeHelperStrategy.doBomMetadataUpdateNewSerialNumber); + } + if (listMergeHelperStrategy.doBomMetadataUpdateReferThisToolkit) + { + result.BomMetadataReferThisToolkit(); + } + if (result.Metadata is null) + { + // If none of the above... + result.Metadata = new Metadata(); + } var toolsMerger = new ListMergeHelper(); var tools = toolsMerger.Merge(bom1.Metadata?.Tools, bom2.Metadata?.Tools, listMergeHelperStrategy); @@ -230,6 +241,8 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // the resulting collection with a lot fewer items to inspect with // the heavier logic. var resultSubj = new Bom(); + // New merged document (new SerialNumber, Version=1, Timestamp)... + resultSubj.BomMetadataUpdate(true); resultSubj.BomMetadataReferThisToolkit(); if (bomSubject is null) @@ -298,10 +311,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); - result.Metadata = new Metadata - { - Timestamp = DateTime.Now - }; + result.BomMetadataUpdate(true); if (bomSubject != null) { From 9b232d50d4a0b3b7c3fbccbaf2ac568e1d23f669 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 30 Aug 2023 16:31:10 +0200 Subject: [PATCH 221/285] Bom.cs: add ability to update Bom document basic metadata (Timestamp, SerialNumber, Version, Metadata/Tools[] for current library and its consumer script) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 130 ++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index ed3c6644..0035652b 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -168,5 +169,132 @@ public int NonNullableVersion [ProtoMember(13)] public List Formulation { get; set; } public bool ShouldSerializeFormulation() { return Formulation?.Count > 0; } + + // TODO: MergeWith() might be reasonable but is currently handled + // by several strategy implementations in CycloneDX.Utils Merge.cs + // so maybe there should be sub-classes or strategy arguments or + // properties to select one of those implementations at run-time?.. + + /// + /// Add reference to this currently running build of cyclonedx-cli + /// (likely) and this cyclonedx-dotnet-library into the Metadata/Tools + /// of this Bom document. Intended for use after processing which + /// creates or modifies the document. After all - any bugs appearing + /// due to library routines are our own and should be trackable... + /// + /// NOTE: Tries to not add identical duplicate entries. + /// + public void BomMetadataReferThisToolkit() + { + // Per https://stackoverflow.com/a/36351902/4715872 : + // Use System.Reflection.Assembly.GetExecutingAssembly() + // to get the assembly (that this line of code is in), or + // use System.Reflection.Assembly.GetEntryAssembly() to + // get the assembly your project started with (most likely + // this is your app). In multi-project solutions this is + // something to keep in mind! + Tool toolThisLibrary = new Tool + { + Vendor = "OWASP Foundation", + Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" + Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() + }; + + if (this.Metadata is null) + { + this.Metadata = new Metadata(); + } + + if (this.Metadata.Tools is null) + { + this.Metadata.Tools = new List(new [] {toolThisLibrary}); + } + else + { + if (!this.Metadata.Tools.Contains(toolThisLibrary)) + { + this.Metadata.Tools.Add(toolThisLibrary); + } + } + + // At worst, these would dedup away?.. + string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar + if (toolThisScriptName != toolThisLibrary.Name) + { + Tool toolThisScript = new Tool + { + Name = toolThisScriptName, + Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), + Version = Assembly.GetEntryAssembly().GetName().Version.ToString() + }; + + if (!this.Metadata.Tools.Contains(toolThisScript)) + { + this.Metadata.Tools.Add(toolThisScript); + } + } + } + + /// + /// Update the Metadata/Timestamp of this Bom document + /// (after content manipulations such as a merge) + /// using DateTime.Now. + /// + /// NOTE: Creates a new Metadata object to populate + /// the property, if one was missing in this Bom object. + /// + public void BomMetadataUpdateTimestamp() + { + if (this.Metadata is null) + { + this.Metadata = new Metadata(); + } + + this.Metadata.Timestamp = DateTime.Now; + } + + /// + /// Update the SerialNumber and optionally bump the Version + /// of a Bom document issued with such serial number (both + /// not in the Metadata structure, but still are "meta data") + /// of this Bom document, either using a new random UUID as + /// the SerialNumber and assigning a Version=1, or bumping + /// the Version -- usually done after content manipulations + /// such as a merge, depending on their caller-defined impact. + /// + public void BomMetadataUpdateSerialNumberVersion(bool generateNewSerialNumber) + { + if (this.Version is null || this.Version < 1 || this.SerialNumber is null || this.SerialNumber == "") + { + generateNewSerialNumber = true; + } + + if (generateNewSerialNumber) + { + this.Version = 1; + this.SerialNumber = "urn:uuid:" + System.Guid.NewGuid().ToString(); + } + else + { + this.Version++; + } + } + + /// + /// Set up (default or update) meta data of this Bom document, + /// covering the Version, SerialNumber and Metadata/Timestamp + /// in one shot. Typically useful to brush up a `new Bom()` or + /// to ensure a new identity for a modified Bom document. + /// + /// NOTE: caller may want to BomMetadataReferThisToolkit() + /// separately, to add the Metadata/Tools[] entries about this + /// CycloneDX library and its consumer (e.g. the "cyclonedx-cli" + /// program). + /// + public void BomMetadataUpdate(bool generateNewSerialNumber) + { + this.BomMetadataUpdateSerialNumberVersion(generateNewSerialNumber); + this.BomMetadataUpdateTimestamp(); + } } -} \ No newline at end of file +} From 00287b93c7fd2d1c33350694bf388c69f5d1da92 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 00:42:10 +0200 Subject: [PATCH 222/285] Merge.cs: use result.BomMetadataUpdate() and BomMetadataReferThisToolkit() to pre-init HierarchicalMerge() and FlatMerge() output object Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index da18352c..831035ae 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -140,6 +140,11 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + // New resulting Bom has its own identity (timestamp, serial) + // and its Tools collection refers to this library and the + // tool which consumes it. + result.BomMetadataUpdate(true); + result.BomMetadataReferThisToolkit(); foreach (var bom in boms) { @@ -192,19 +197,16 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); + // New resulting Bom has its own identity (timestamp, serial) + // and its Tools collection refers to this library and the + // tool which consumes it. + result.BomMetadataUpdate(true); + result.BomMetadataReferThisToolkit(); + if (bomSubject != null) { if (bomSubject.BomRef is null) bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); - result.Metadata = new Metadata - { - Component = bomSubject, - #pragma warning disable 618 - Tools = new ToolChoices - { - Tools = new List(), - } - #pragma warning restore 618 - }; + result.Metadata.Component = bomSubject; } result.Components = new List(); From cd750b9a17c683bf50bd7540c921fdc40ee2009f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 00:47:00 +0200 Subject: [PATCH 223/285] Bom.cs: modernize BomMetadataReferThisToolkit() for cyclonedx-dotnet-library 6.0.0 with its intermediate ToolChoices type (for CDX spec 1.5) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 0035652b..11f55467 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -205,15 +205,20 @@ public void BomMetadataReferThisToolkit() this.Metadata = new Metadata(); } - if (this.Metadata.Tools is null) + if (this.Metadata.Tools is null || this.Metadata.Tools.Tools is null) { - this.Metadata.Tools = new List(new [] {toolThisLibrary}); + #pragma warning disable 618 + this.Metadata.Tools = new ToolChoices + { + Tools = new List(new [] {toolThisLibrary}), + } + #pragma warning restore 618 } else { - if (!this.Metadata.Tools.Contains(toolThisLibrary)) + if (!this.Metadata.Tools.Tools.Contains(toolThisLibrary)) { - this.Metadata.Tools.Add(toolThisLibrary); + this.Metadata.Tools.Tools.Add(toolThisLibrary); } } @@ -228,9 +233,9 @@ public void BomMetadataReferThisToolkit() Version = Assembly.GetEntryAssembly().GetName().Version.ToString() }; - if (!this.Metadata.Tools.Contains(toolThisScript)) + if (!this.Metadata.Tools.Tools.Contains(toolThisScript)) { - this.Metadata.Tools.Add(toolThisScript); + this.Metadata.Tools.Tools.Add(toolThisScript); } } } From d12445ca7089d673e478b7901268b748e9b5b8db Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 01:32:44 +0200 Subject: [PATCH 224/285] Bom.cs: BomMetadataUpdateSerialNumberVersion(): address static analysis concerns Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 11f55467..516b8f67 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -269,12 +269,13 @@ public void BomMetadataUpdateTimestamp() /// public void BomMetadataUpdateSerialNumberVersion(bool generateNewSerialNumber) { + bool doGenerateNewSerialNumber = generateNewSerialNumber; if (this.Version is null || this.Version < 1 || this.SerialNumber is null || this.SerialNumber == "") { - generateNewSerialNumber = true; + doGenerateNewSerialNumber = true; } - if (generateNewSerialNumber) + if (doGenerateNewSerialNumber) { this.Version = 1; this.SerialNumber = "urn:uuid:" + System.Guid.NewGuid().ToString(); From cdbb1391037681bac5d890f1ac5a80f106803d88 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 01:51:13 +0200 Subject: [PATCH 225/285] Merge.cs: FlatMerge(): if we did pre-populate result...Tools[], then do not overwrite it with the incoming set of merged values - merge them again Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index a0351663..6d494b92 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -110,6 +110,16 @@ public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy var tools = toolsMerger.Merge(bom1.Metadata?.Tools?.Tools, bom2.Metadata?.Tools?.Tools, listMergeHelperStrategy); if (tools != null) { + if (result.Metadata.Tools == null) + { + result.Metadata.Tools = new ToolChoices(); + } + + if (result.Metadata.Tools.Tools != null) + { + tools = toolsMerger.Merge(result.Metadata.Tools.Tools, tools, listMergeHelperStrategy); + } + result.Metadata.Tools.Tools = tools; } From 9fdf96e8179953a885a76114a531b0aa72ab9ff2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 01:51:50 +0200 Subject: [PATCH 226/285] Merge.cs: HierarchicalMerge(): we did pre-populate result...Tools[], no need to check and reinit them Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 6d494b92..725dc087 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -361,16 +361,6 @@ bom.SerialNumber is null if (bom.Metadata?.Tools?.Tools?.Count > 0) { - if (result.Metadata.Tools?.Tools == null) - { - #pragma warning disable 618 - result.Metadata.Tools = new ToolChoices - { - Tools = new List(), - } - #pragma warning restore 618 - } - result.Metadata.Tools.Tools.AddRange(bom.Metadata.Tools.Tools); } From 2e87f5dbd5ea7f5af5b6e3509a60522a88696dc6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 02:34:33 +0200 Subject: [PATCH 227/285] Bom.cs: fix compilation warnings with obsoleted Tool class Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 516b8f67..e25799a0 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -193,12 +193,14 @@ public void BomMetadataReferThisToolkit() // get the assembly your project started with (most likely // this is your app). In multi-project solutions this is // something to keep in mind! + #pragma warning disable 618 Tool toolThisLibrary = new Tool { Vendor = "OWASP Foundation", Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() }; + #pragma warning restore 618 if (this.Metadata is null) { @@ -211,7 +213,7 @@ public void BomMetadataReferThisToolkit() this.Metadata.Tools = new ToolChoices { Tools = new List(new [] {toolThisLibrary}), - } + }; #pragma warning restore 618 } else @@ -226,12 +228,14 @@ public void BomMetadataReferThisToolkit() string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar if (toolThisScriptName != toolThisLibrary.Name) { + #pragma warning disable 618 Tool toolThisScript = new Tool { Name = toolThisScriptName, Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), Version = Assembly.GetEntryAssembly().GetName().Version.ToString() }; + #pragma warning restore 618 if (!this.Metadata.Tools.Tools.Contains(toolThisScript)) { From d29e3303b760a1c39fb68404b5a55958c54dde2e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 Sep 2023 03:07:28 +0200 Subject: [PATCH 228/285] Merge.cs: fix compilation warnings with obsoleted Tool class Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 2a7d9282..64f8ef7e 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -236,12 +236,14 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) // get the assembly your project started with (most likely // this is your app). In multi-project solutions this is // something to keep in mind! + #pragma warning disable 618 Tool toolThisLibrary = new Tool { Vendor = "OWASP Foundation", Name = Assembly.GetExecutingAssembly().GetName().Name, // "cyclonedx-dotnet-library" Version = Assembly.GetExecutingAssembly().GetName().Version.ToString() }; + #pragma warning restore 618 resultSubj.Metadata = new Metadata { @@ -257,13 +259,15 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) string toolThisScriptName = Assembly.GetEntryAssembly().GetName().Name; // "cyclonedx-cli" or similar if (toolThisScriptName != toolThisLibrary.Name) { + #pragma warning disable 618 Tool toolThisScript = new Tool { Name = toolThisScriptName, Vendor = (toolThisScriptName.ToLowerInvariant().StartsWith("cyclonedx") ? "OWASP Foundation" : null), Version = Assembly.GetEntryAssembly().GetName().Version.ToString() }; - resultSubj.Metadata.Tools.Add(toolThisScript); + #pragma warning restore 618 + resultSubj.Metadata.Tools.Tools.Add(toolThisScript); } From 9e808cb56fc443f0dcafbc8e7c70c41a7ba8fe56 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 22 Sep 2023 12:41:20 +0200 Subject: [PATCH 229/285] Make new classes introduced with cyclonedx-dotnet-library v6.0.0 release (for CycloneDX 1.5 spec) descendants of BomEntity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 4 ++-- src/CycloneDX.Core/Models/AnnotatorChoice.cs | 2 +- src/CycloneDX.Core/Models/Callstack.cs | 4 ++-- src/CycloneDX.Core/Models/Command.cs | 2 +- src/CycloneDX.Core/Models/Data.cs | 4 ++-- src/CycloneDX.Core/Models/DataFlow.cs | 2 +- src/CycloneDX.Core/Models/DataGovernance.cs | 2 +- src/CycloneDX.Core/Models/DataflowSourceDestination.cs | 2 +- src/CycloneDX.Core/Models/DatasetChoice.cs | 2 +- src/CycloneDX.Core/Models/EnvironmentVarChoice.cs | 2 +- src/CycloneDX.Core/Models/Event.cs | 2 +- src/CycloneDX.Core/Models/EvidenceIdentity.cs | 2 +- src/CycloneDX.Core/Models/EvidenceMethods.cs | 2 +- src/CycloneDX.Core/Models/EvidenceOccurrence.cs | 2 +- src/CycloneDX.Core/Models/Formula.cs | 2 +- src/CycloneDX.Core/Models/GraphicsCollection.cs | 4 ++-- src/CycloneDX.Core/Models/Input.cs | 2 +- src/CycloneDX.Core/Models/Licensing.cs | 2 +- src/CycloneDX.Core/Models/Lifecycles.cs | 2 +- src/CycloneDX.Core/Models/ModelCard.cs | 8 ++++---- src/CycloneDX.Core/Models/ModelCardConsiderations.cs | 6 +++--- src/CycloneDX.Core/Models/ModelParameters.cs | 8 ++++---- .../Models/OrganizationalEntityOrContact.cs | 2 +- src/CycloneDX.Core/Models/Output.cs | 2 +- src/CycloneDX.Core/Models/Parameter.cs | 2 +- src/CycloneDX.Core/Models/ResourceReferenceChoice.cs | 2 +- src/CycloneDX.Core/Models/ServiceDataChoices.cs | 2 +- src/CycloneDX.Core/Models/Step.cs | 2 +- src/CycloneDX.Core/Models/ToolChoices.cs | 2 +- src/CycloneDX.Core/Models/Trigger.cs | 4 ++-- src/CycloneDX.Core/Models/Volume.cs | 2 +- .../Models/Vulnerabilities/ProofOfConcept.cs | 2 +- src/CycloneDX.Core/Models/Workflow.cs | 2 +- src/CycloneDX.Core/Models/WorkflowTask.cs | 2 +- src/CycloneDX.Core/Models/Workspace.cs | 2 +- 35 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index bf10b666..4fc89167 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -24,10 +24,10 @@ namespace CycloneDX.Models { [ProtoContract] - public class Annotation + public class Annotation : BomEntity { [XmlType("subject")] - public class XmlAnnotationSubject + public class XmlAnnotationSubject : BomEntity { [XmlAttribute("ref")] public string Ref { get; set; } diff --git a/src/CycloneDX.Core/Models/AnnotatorChoice.cs b/src/CycloneDX.Core/Models/AnnotatorChoice.cs index b996fec3..5e5051cb 100644 --- a/src/CycloneDX.Core/Models/AnnotatorChoice.cs +++ b/src/CycloneDX.Core/Models/AnnotatorChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class AnnotatorChoice + public class AnnotatorChoice : BomEntity { [XmlElement("organization")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Callstack.cs b/src/CycloneDX.Core/Models/Callstack.cs index 522d68db..da26c4f1 100644 --- a/src/CycloneDX.Core/Models/Callstack.cs +++ b/src/CycloneDX.Core/Models/Callstack.cs @@ -28,11 +28,11 @@ namespace CycloneDX.Models { [XmlType("callstack")] [ProtoContract] - public class Callstack + public class Callstack : BomEntity { [XmlType("frame")] [ProtoContract] - public class Frame + public class Frame : BomEntity { [XmlElement("package")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Command.cs b/src/CycloneDX.Core/Models/Command.cs index f0bfc1ff..4b319a88 100644 --- a/src/CycloneDX.Core/Models/Command.cs +++ b/src/CycloneDX.Core/Models/Command.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("command")] [ProtoContract] - public class Command + public class Command : BomEntity { [XmlElement("executed")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Data.cs b/src/CycloneDX.Core/Models/Data.cs index 7741a68a..95fc7d7e 100644 --- a/src/CycloneDX.Core/Models/Data.cs +++ b/src/CycloneDX.Core/Models/Data.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Data + public class Data : BomEntity { [ProtoContract] public enum DataType @@ -43,7 +43,7 @@ public enum DataType } [ProtoContract] - public class DataContents + public class DataContents : BomEntity { [XmlElement("attachment")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/DataFlow.cs b/src/CycloneDX.Core/Models/DataFlow.cs index 099d6d1b..24803811 100644 --- a/src/CycloneDX.Core/Models/DataFlow.cs +++ b/src/CycloneDX.Core/Models/DataFlow.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [XmlType("dataflow")] [ProtoContract] - public class DataFlow + public class DataFlow : BomEntity { [XmlIgnore] [JsonPropertyName("flow")] diff --git a/src/CycloneDX.Core/Models/DataGovernance.cs b/src/CycloneDX.Core/Models/DataGovernance.cs index 0b8b939e..307af00d 100644 --- a/src/CycloneDX.Core/Models/DataGovernance.cs +++ b/src/CycloneDX.Core/Models/DataGovernance.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("data-governance")] [ProtoContract] - public class DataGovernance + public class DataGovernance : BomEntity { [XmlArray("custodians")] [XmlArrayItem("custodian")] diff --git a/src/CycloneDX.Core/Models/DataflowSourceDestination.cs b/src/CycloneDX.Core/Models/DataflowSourceDestination.cs index e2b4157b..96f4b569 100644 --- a/src/CycloneDX.Core/Models/DataflowSourceDestination.cs +++ b/src/CycloneDX.Core/Models/DataflowSourceDestination.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DataflowSourceDestination + public class DataflowSourceDestination : BomEntity { [XmlElement("url")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/DatasetChoice.cs b/src/CycloneDX.Core/Models/DatasetChoice.cs index c50d9684..ddae69cf 100644 --- a/src/CycloneDX.Core/Models/DatasetChoice.cs +++ b/src/CycloneDX.Core/Models/DatasetChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DatasetChoice + public class DatasetChoice : BomEntity { [XmlElement("dataset")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs b/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs index 733b6ed0..5d8a8066 100644 --- a/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs +++ b/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class EnvironmentVarChoice + public class EnvironmentVarChoice : BomEntity { [ProtoMember(1)] public Property Property { get; set; } diff --git a/src/CycloneDX.Core/Models/Event.cs b/src/CycloneDX.Core/Models/Event.cs index ae9d4196..86286c01 100644 --- a/src/CycloneDX.Core/Models/Event.cs +++ b/src/CycloneDX.Core/Models/Event.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("event")] [ProtoContract] - public class Event + public class Event : BomEntity { [XmlElement("uid")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EvidenceIdentity.cs b/src/CycloneDX.Core/Models/EvidenceIdentity.cs index a9c3c18a..82cce450 100644 --- a/src/CycloneDX.Core/Models/EvidenceIdentity.cs +++ b/src/CycloneDX.Core/Models/EvidenceIdentity.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-identity")] [ProtoContract] - public class EvidenceIdentity + public class EvidenceIdentity : BomEntity { [ProtoContract] public enum EvidenceFieldType diff --git a/src/CycloneDX.Core/Models/EvidenceMethods.cs b/src/CycloneDX.Core/Models/EvidenceMethods.cs index dce4170e..6b0898ba 100644 --- a/src/CycloneDX.Core/Models/EvidenceMethods.cs +++ b/src/CycloneDX.Core/Models/EvidenceMethods.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-methods")] [ProtoContract] - public class EvidenceMethods + public class EvidenceMethods : BomEntity { [ProtoContract] public enum EvidenceTechnique diff --git a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs index 126ce97a..32beb5e2 100644 --- a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs +++ b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-occurrence")] [ProtoContract] - public class EvidenceOccurrence + public class EvidenceOccurrence : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Formula.cs b/src/CycloneDX.Core/Models/Formula.cs index d0b5ffed..95b4866c 100644 --- a/src/CycloneDX.Core/Models/Formula.cs +++ b/src/CycloneDX.Core/Models/Formula.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("formula")] [ProtoContract] - public class Formula + public class Formula : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/GraphicsCollection.cs b/src/CycloneDX.Core/Models/GraphicsCollection.cs index cc35bb19..200f95e0 100644 --- a/src/CycloneDX.Core/Models/GraphicsCollection.cs +++ b/src/CycloneDX.Core/Models/GraphicsCollection.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("graphics")] [ProtoContract] - public class GraphicsCollection + public class GraphicsCollection : BomEntity { [ProtoContract] - public class Graphic + public class Graphic : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Input.cs b/src/CycloneDX.Core/Models/Input.cs index d8366765..4d26bb99 100644 --- a/src/CycloneDX.Core/Models/Input.cs +++ b/src/CycloneDX.Core/Models/Input.cs @@ -27,7 +27,7 @@ namespace CycloneDX.Models { [XmlType("input")] [ProtoContract] - public class Input + public class Input : BomEntity { [XmlElement("resource")] [ProtoMember(3)] diff --git a/src/CycloneDX.Core/Models/Licensing.cs b/src/CycloneDX.Core/Models/Licensing.cs index ac62118a..ee6eb079 100644 --- a/src/CycloneDX.Core/Models/Licensing.cs +++ b/src/CycloneDX.Core/Models/Licensing.cs @@ -27,7 +27,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("licensing")] [ProtoContract] - public class Licensing + public class Licensing : BomEntity { [ProtoContract] public enum LicenseType diff --git a/src/CycloneDX.Core/Models/Lifecycles.cs b/src/CycloneDX.Core/Models/Lifecycles.cs index 48165d47..99ab9531 100644 --- a/src/CycloneDX.Core/Models/Lifecycles.cs +++ b/src/CycloneDX.Core/Models/Lifecycles.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Lifecycles + public class Lifecycles : BomEntity { [ProtoContract] public enum LifecyclePhase diff --git a/src/CycloneDX.Core/Models/ModelCard.cs b/src/CycloneDX.Core/Models/ModelCard.cs index 00afe51e..8ce27b92 100644 --- a/src/CycloneDX.Core/Models/ModelCard.cs +++ b/src/CycloneDX.Core/Models/ModelCard.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("modelCard")] [ProtoContract] - public class ModelCard + public class ModelCard : BomEntity { [ProtoContract] public enum ModelParameterApproachType @@ -44,13 +44,13 @@ public enum ModelParameterApproachType } [ProtoContract] - public class ModelCardQuantitativeAnalysis + public class ModelCardQuantitativeAnalysis : BomEntity { [ProtoContract] - public class PerformanceMetric + public class PerformanceMetric : BomEntity { [ProtoContract] - public class PerformanceMetricConfidenceInterval + public class PerformanceMetricConfidenceInterval : BomEntity { [XmlElement("lowerBound")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ModelCardConsiderations.cs b/src/CycloneDX.Core/Models/ModelCardConsiderations.cs index d42c88b7..4f50054d 100644 --- a/src/CycloneDX.Core/Models/ModelCardConsiderations.cs +++ b/src/CycloneDX.Core/Models/ModelCardConsiderations.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("modelCardConsiderations")] [ProtoContract] - public class ModelCardConsiderations + public class ModelCardConsiderations : BomEntity { [ProtoContract] - public class ModelCardEthicalConsideration + public class ModelCardEthicalConsideration : BomEntity { [XmlElement("name")] [ProtoMember(1)] @@ -41,7 +41,7 @@ public class ModelCardEthicalConsideration } [ProtoContract] - public class ModelCardFairnessAssessment + public class ModelCardFairnessAssessment : BomEntity { [XmlElement("groupAtRisk")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ModelParameters.cs b/src/CycloneDX.Core/Models/ModelParameters.cs index a568bb54..f2ce7e86 100644 --- a/src/CycloneDX.Core/Models/ModelParameters.cs +++ b/src/CycloneDX.Core/Models/ModelParameters.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("model-parameters")] [ProtoContract] - public class ModelParameters + public class ModelParameters : BomEntity { [ProtoContract] - public class ModelApproach + public class ModelApproach : BomEntity { [XmlElement("type")] [ProtoMember(1)] @@ -37,7 +37,7 @@ public class ModelApproach } [ProtoContract] - public class ModelDataset + public class ModelDataset : BomEntity { [XmlElement("dataset")] [ProtoMember(1)] @@ -49,7 +49,7 @@ public class ModelDataset } [ProtoContract] - public class MachineLearningInputOutputParameter + public class MachineLearningInputOutputParameter : BomEntity { [XmlElement("format")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs b/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs index 9db9fb40..8439edc6 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntityOrContact + public class OrganizationalEntityOrContact : BomEntity { [XmlElement("organization")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Output.cs b/src/CycloneDX.Core/Models/Output.cs index 001e227b..f41cc94b 100644 --- a/src/CycloneDX.Core/Models/Output.cs +++ b/src/CycloneDX.Core/Models/Output.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("output")] [ProtoContract] - public class Output + public class Output : BomEntity { [ProtoContract] public enum OutputType diff --git a/src/CycloneDX.Core/Models/Parameter.cs b/src/CycloneDX.Core/Models/Parameter.cs index 4de2b485..2427712f 100644 --- a/src/CycloneDX.Core/Models/Parameter.cs +++ b/src/CycloneDX.Core/Models/Parameter.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Parameter + public class Parameter : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs index dc8153b4..55451d48 100644 --- a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs +++ b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ResourceReferenceChoice : IXmlSerializable + public class ResourceReferenceChoice : BomEntity, IXmlSerializable { private static XmlSerializer _extRefSerializer; private static XmlSerializer GetExternalReferenceSerializer() diff --git a/src/CycloneDX.Core/Models/ServiceDataChoices.cs b/src/CycloneDX.Core/Models/ServiceDataChoices.cs index 51a64076..0ace05c2 100644 --- a/src/CycloneDX.Core/Models/ServiceDataChoices.cs +++ b/src/CycloneDX.Core/Models/ServiceDataChoices.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ServiceDataChoices : IXmlSerializable + public class ServiceDataChoices : BomEntity, IXmlSerializable { internal SpecificationVersion SpecVersion { get; set; } diff --git a/src/CycloneDX.Core/Models/Step.cs b/src/CycloneDX.Core/Models/Step.cs index 962c1e1e..b52a28dd 100644 --- a/src/CycloneDX.Core/Models/Step.cs +++ b/src/CycloneDX.Core/Models/Step.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("step")] [ProtoContract] - public class Step + public class Step : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ToolChoices.cs b/src/CycloneDX.Core/Models/ToolChoices.cs index e9059e57..3f56d952 100644 --- a/src/CycloneDX.Core/Models/ToolChoices.cs +++ b/src/CycloneDX.Core/Models/ToolChoices.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ToolChoices : IXmlSerializable + public class ToolChoices : BomEntity, IXmlSerializable { internal SpecificationVersion SpecVersion { get; set; } diff --git a/src/CycloneDX.Core/Models/Trigger.cs b/src/CycloneDX.Core/Models/Trigger.cs index 5b8407b2..2aa1bd88 100644 --- a/src/CycloneDX.Core/Models/Trigger.cs +++ b/src/CycloneDX.Core/Models/Trigger.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("trigger")] [ProtoContract] - public class Trigger + public class Trigger : BomEntity { [ProtoContract] public enum TriggerType @@ -42,7 +42,7 @@ public enum TriggerType } [ProtoContract] - public class Condition + public class Condition : BomEntity { [XmlElement("description")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Volume.cs b/src/CycloneDX.Core/Models/Volume.cs index 25a76eeb..d4ed0432 100644 --- a/src/CycloneDX.Core/Models/Volume.cs +++ b/src/CycloneDX.Core/Models/Volume.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("volume")] [ProtoContract] - public class Volume + public class Volume : BomEntity { [ProtoContract] public enum VolumeMode diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs b/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs index 9017d166..dea429c3 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class ProofOfConcept + public class ProofOfConcept : BomEntity { [XmlElement("reproductionSteps")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Workflow.cs b/src/CycloneDX.Core/Models/Workflow.cs index b74a9cd2..ed52c85c 100644 --- a/src/CycloneDX.Core/Models/Workflow.cs +++ b/src/CycloneDX.Core/Models/Workflow.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workflow")] [ProtoContract] - public class Workflow + public class Workflow : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/WorkflowTask.cs b/src/CycloneDX.Core/Models/WorkflowTask.cs index 9eb0d514..bf2efeb6 100644 --- a/src/CycloneDX.Core/Models/WorkflowTask.cs +++ b/src/CycloneDX.Core/Models/WorkflowTask.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("task")] [ProtoContract] - public class WorkflowTask + public class WorkflowTask : BomEntity { [ProtoContract] public enum TaskType diff --git a/src/CycloneDX.Core/Models/Workspace.cs b/src/CycloneDX.Core/Models/Workspace.cs index ccc43f79..d9fac743 100644 --- a/src/CycloneDX.Core/Models/Workspace.cs +++ b/src/CycloneDX.Core/Models/Workspace.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workspace")] [ProtoContract] - public class Workspace + public class Workspace : BomEntity { [ProtoContract] public enum AccessModeType From 48291018410874dce841449eae45db1905f49132 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 22 Sep 2023 12:41:20 +0200 Subject: [PATCH 230/285] Make new classes introduced with cyclonedx-dotnet-library v6.0.0 release (for CycloneDX 1.5 spec) descendants of BomEntity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 4 ++-- src/CycloneDX.Core/Models/AnnotatorChoice.cs | 2 +- src/CycloneDX.Core/Models/Callstack.cs | 4 ++-- src/CycloneDX.Core/Models/Command.cs | 2 +- src/CycloneDX.Core/Models/Data.cs | 4 ++-- src/CycloneDX.Core/Models/DataFlow.cs | 2 +- src/CycloneDX.Core/Models/DataGovernance.cs | 2 +- src/CycloneDX.Core/Models/DataflowSourceDestination.cs | 2 +- src/CycloneDX.Core/Models/DatasetChoice.cs | 2 +- src/CycloneDX.Core/Models/EnvironmentVarChoice.cs | 2 +- src/CycloneDX.Core/Models/Event.cs | 2 +- src/CycloneDX.Core/Models/EvidenceIdentity.cs | 2 +- src/CycloneDX.Core/Models/EvidenceMethods.cs | 2 +- src/CycloneDX.Core/Models/EvidenceOccurrence.cs | 2 +- src/CycloneDX.Core/Models/Formula.cs | 2 +- src/CycloneDX.Core/Models/GraphicsCollection.cs | 4 ++-- src/CycloneDX.Core/Models/Input.cs | 2 +- src/CycloneDX.Core/Models/Licensing.cs | 2 +- src/CycloneDX.Core/Models/Lifecycles.cs | 2 +- src/CycloneDX.Core/Models/ModelCard.cs | 8 ++++---- src/CycloneDX.Core/Models/ModelCardConsiderations.cs | 6 +++--- src/CycloneDX.Core/Models/ModelParameters.cs | 8 ++++---- .../Models/OrganizationalEntityOrContact.cs | 2 +- src/CycloneDX.Core/Models/Output.cs | 2 +- src/CycloneDX.Core/Models/Parameter.cs | 2 +- src/CycloneDX.Core/Models/ResourceReferenceChoice.cs | 2 +- src/CycloneDX.Core/Models/ServiceDataChoices.cs | 2 +- src/CycloneDX.Core/Models/Step.cs | 2 +- src/CycloneDX.Core/Models/ToolChoices.cs | 2 +- src/CycloneDX.Core/Models/Trigger.cs | 4 ++-- src/CycloneDX.Core/Models/Volume.cs | 2 +- .../Models/Vulnerabilities/ProofOfConcept.cs | 2 +- src/CycloneDX.Core/Models/Workflow.cs | 2 +- src/CycloneDX.Core/Models/WorkflowTask.cs | 2 +- src/CycloneDX.Core/Models/Workspace.cs | 2 +- 35 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index bf10b666..4fc89167 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -24,10 +24,10 @@ namespace CycloneDX.Models { [ProtoContract] - public class Annotation + public class Annotation : BomEntity { [XmlType("subject")] - public class XmlAnnotationSubject + public class XmlAnnotationSubject : BomEntity { [XmlAttribute("ref")] public string Ref { get; set; } diff --git a/src/CycloneDX.Core/Models/AnnotatorChoice.cs b/src/CycloneDX.Core/Models/AnnotatorChoice.cs index b996fec3..5e5051cb 100644 --- a/src/CycloneDX.Core/Models/AnnotatorChoice.cs +++ b/src/CycloneDX.Core/Models/AnnotatorChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class AnnotatorChoice + public class AnnotatorChoice : BomEntity { [XmlElement("organization")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Callstack.cs b/src/CycloneDX.Core/Models/Callstack.cs index 522d68db..da26c4f1 100644 --- a/src/CycloneDX.Core/Models/Callstack.cs +++ b/src/CycloneDX.Core/Models/Callstack.cs @@ -28,11 +28,11 @@ namespace CycloneDX.Models { [XmlType("callstack")] [ProtoContract] - public class Callstack + public class Callstack : BomEntity { [XmlType("frame")] [ProtoContract] - public class Frame + public class Frame : BomEntity { [XmlElement("package")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Command.cs b/src/CycloneDX.Core/Models/Command.cs index f0bfc1ff..4b319a88 100644 --- a/src/CycloneDX.Core/Models/Command.cs +++ b/src/CycloneDX.Core/Models/Command.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("command")] [ProtoContract] - public class Command + public class Command : BomEntity { [XmlElement("executed")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Data.cs b/src/CycloneDX.Core/Models/Data.cs index 7741a68a..95fc7d7e 100644 --- a/src/CycloneDX.Core/Models/Data.cs +++ b/src/CycloneDX.Core/Models/Data.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Data + public class Data : BomEntity { [ProtoContract] public enum DataType @@ -43,7 +43,7 @@ public enum DataType } [ProtoContract] - public class DataContents + public class DataContents : BomEntity { [XmlElement("attachment")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/DataFlow.cs b/src/CycloneDX.Core/Models/DataFlow.cs index 099d6d1b..24803811 100644 --- a/src/CycloneDX.Core/Models/DataFlow.cs +++ b/src/CycloneDX.Core/Models/DataFlow.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [XmlType("dataflow")] [ProtoContract] - public class DataFlow + public class DataFlow : BomEntity { [XmlIgnore] [JsonPropertyName("flow")] diff --git a/src/CycloneDX.Core/Models/DataGovernance.cs b/src/CycloneDX.Core/Models/DataGovernance.cs index 0b8b939e..307af00d 100644 --- a/src/CycloneDX.Core/Models/DataGovernance.cs +++ b/src/CycloneDX.Core/Models/DataGovernance.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("data-governance")] [ProtoContract] - public class DataGovernance + public class DataGovernance : BomEntity { [XmlArray("custodians")] [XmlArrayItem("custodian")] diff --git a/src/CycloneDX.Core/Models/DataflowSourceDestination.cs b/src/CycloneDX.Core/Models/DataflowSourceDestination.cs index e2b4157b..96f4b569 100644 --- a/src/CycloneDX.Core/Models/DataflowSourceDestination.cs +++ b/src/CycloneDX.Core/Models/DataflowSourceDestination.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DataflowSourceDestination + public class DataflowSourceDestination : BomEntity { [XmlElement("url")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/DatasetChoice.cs b/src/CycloneDX.Core/Models/DatasetChoice.cs index c50d9684..ddae69cf 100644 --- a/src/CycloneDX.Core/Models/DatasetChoice.cs +++ b/src/CycloneDX.Core/Models/DatasetChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DatasetChoice + public class DatasetChoice : BomEntity { [XmlElement("dataset")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs b/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs index 733b6ed0..5d8a8066 100644 --- a/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs +++ b/src/CycloneDX.Core/Models/EnvironmentVarChoice.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class EnvironmentVarChoice + public class EnvironmentVarChoice : BomEntity { [ProtoMember(1)] public Property Property { get; set; } diff --git a/src/CycloneDX.Core/Models/Event.cs b/src/CycloneDX.Core/Models/Event.cs index ae9d4196..86286c01 100644 --- a/src/CycloneDX.Core/Models/Event.cs +++ b/src/CycloneDX.Core/Models/Event.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("event")] [ProtoContract] - public class Event + public class Event : BomEntity { [XmlElement("uid")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EvidenceIdentity.cs b/src/CycloneDX.Core/Models/EvidenceIdentity.cs index a9c3c18a..82cce450 100644 --- a/src/CycloneDX.Core/Models/EvidenceIdentity.cs +++ b/src/CycloneDX.Core/Models/EvidenceIdentity.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-identity")] [ProtoContract] - public class EvidenceIdentity + public class EvidenceIdentity : BomEntity { [ProtoContract] public enum EvidenceFieldType diff --git a/src/CycloneDX.Core/Models/EvidenceMethods.cs b/src/CycloneDX.Core/Models/EvidenceMethods.cs index dce4170e..6b0898ba 100644 --- a/src/CycloneDX.Core/Models/EvidenceMethods.cs +++ b/src/CycloneDX.Core/Models/EvidenceMethods.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-methods")] [ProtoContract] - public class EvidenceMethods + public class EvidenceMethods : BomEntity { [ProtoContract] public enum EvidenceTechnique diff --git a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs index 126ce97a..32beb5e2 100644 --- a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs +++ b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-occurrence")] [ProtoContract] - public class EvidenceOccurrence + public class EvidenceOccurrence : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Formula.cs b/src/CycloneDX.Core/Models/Formula.cs index d0b5ffed..95b4866c 100644 --- a/src/CycloneDX.Core/Models/Formula.cs +++ b/src/CycloneDX.Core/Models/Formula.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("formula")] [ProtoContract] - public class Formula + public class Formula : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/GraphicsCollection.cs b/src/CycloneDX.Core/Models/GraphicsCollection.cs index cc35bb19..200f95e0 100644 --- a/src/CycloneDX.Core/Models/GraphicsCollection.cs +++ b/src/CycloneDX.Core/Models/GraphicsCollection.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("graphics")] [ProtoContract] - public class GraphicsCollection + public class GraphicsCollection : BomEntity { [ProtoContract] - public class Graphic + public class Graphic : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Input.cs b/src/CycloneDX.Core/Models/Input.cs index d8366765..4d26bb99 100644 --- a/src/CycloneDX.Core/Models/Input.cs +++ b/src/CycloneDX.Core/Models/Input.cs @@ -27,7 +27,7 @@ namespace CycloneDX.Models { [XmlType("input")] [ProtoContract] - public class Input + public class Input : BomEntity { [XmlElement("resource")] [ProtoMember(3)] diff --git a/src/CycloneDX.Core/Models/Licensing.cs b/src/CycloneDX.Core/Models/Licensing.cs index ac62118a..ee6eb079 100644 --- a/src/CycloneDX.Core/Models/Licensing.cs +++ b/src/CycloneDX.Core/Models/Licensing.cs @@ -27,7 +27,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("licensing")] [ProtoContract] - public class Licensing + public class Licensing : BomEntity { [ProtoContract] public enum LicenseType diff --git a/src/CycloneDX.Core/Models/Lifecycles.cs b/src/CycloneDX.Core/Models/Lifecycles.cs index 48165d47..99ab9531 100644 --- a/src/CycloneDX.Core/Models/Lifecycles.cs +++ b/src/CycloneDX.Core/Models/Lifecycles.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Lifecycles + public class Lifecycles : BomEntity { [ProtoContract] public enum LifecyclePhase diff --git a/src/CycloneDX.Core/Models/ModelCard.cs b/src/CycloneDX.Core/Models/ModelCard.cs index 00afe51e..8ce27b92 100644 --- a/src/CycloneDX.Core/Models/ModelCard.cs +++ b/src/CycloneDX.Core/Models/ModelCard.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("modelCard")] [ProtoContract] - public class ModelCard + public class ModelCard : BomEntity { [ProtoContract] public enum ModelParameterApproachType @@ -44,13 +44,13 @@ public enum ModelParameterApproachType } [ProtoContract] - public class ModelCardQuantitativeAnalysis + public class ModelCardQuantitativeAnalysis : BomEntity { [ProtoContract] - public class PerformanceMetric + public class PerformanceMetric : BomEntity { [ProtoContract] - public class PerformanceMetricConfidenceInterval + public class PerformanceMetricConfidenceInterval : BomEntity { [XmlElement("lowerBound")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ModelCardConsiderations.cs b/src/CycloneDX.Core/Models/ModelCardConsiderations.cs index d42c88b7..4f50054d 100644 --- a/src/CycloneDX.Core/Models/ModelCardConsiderations.cs +++ b/src/CycloneDX.Core/Models/ModelCardConsiderations.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("modelCardConsiderations")] [ProtoContract] - public class ModelCardConsiderations + public class ModelCardConsiderations : BomEntity { [ProtoContract] - public class ModelCardEthicalConsideration + public class ModelCardEthicalConsideration : BomEntity { [XmlElement("name")] [ProtoMember(1)] @@ -41,7 +41,7 @@ public class ModelCardEthicalConsideration } [ProtoContract] - public class ModelCardFairnessAssessment + public class ModelCardFairnessAssessment : BomEntity { [XmlElement("groupAtRisk")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ModelParameters.cs b/src/CycloneDX.Core/Models/ModelParameters.cs index a568bb54..f2ce7e86 100644 --- a/src/CycloneDX.Core/Models/ModelParameters.cs +++ b/src/CycloneDX.Core/Models/ModelParameters.cs @@ -26,10 +26,10 @@ namespace CycloneDX.Models { [XmlType("model-parameters")] [ProtoContract] - public class ModelParameters + public class ModelParameters : BomEntity { [ProtoContract] - public class ModelApproach + public class ModelApproach : BomEntity { [XmlElement("type")] [ProtoMember(1)] @@ -37,7 +37,7 @@ public class ModelApproach } [ProtoContract] - public class ModelDataset + public class ModelDataset : BomEntity { [XmlElement("dataset")] [ProtoMember(1)] @@ -49,7 +49,7 @@ public class ModelDataset } [ProtoContract] - public class MachineLearningInputOutputParameter + public class MachineLearningInputOutputParameter : BomEntity { [XmlElement("format")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs b/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs index 9db9fb40..8439edc6 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntityOrContact.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntityOrContact + public class OrganizationalEntityOrContact : BomEntity { [XmlElement("organization")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Output.cs b/src/CycloneDX.Core/Models/Output.cs index 001e227b..f41cc94b 100644 --- a/src/CycloneDX.Core/Models/Output.cs +++ b/src/CycloneDX.Core/Models/Output.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("output")] [ProtoContract] - public class Output + public class Output : BomEntity { [ProtoContract] public enum OutputType diff --git a/src/CycloneDX.Core/Models/Parameter.cs b/src/CycloneDX.Core/Models/Parameter.cs index 4de2b485..2427712f 100644 --- a/src/CycloneDX.Core/Models/Parameter.cs +++ b/src/CycloneDX.Core/Models/Parameter.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Parameter + public class Parameter : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs index dc8153b4..55451d48 100644 --- a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs +++ b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ResourceReferenceChoice : IXmlSerializable + public class ResourceReferenceChoice : BomEntity, IXmlSerializable { private static XmlSerializer _extRefSerializer; private static XmlSerializer GetExternalReferenceSerializer() diff --git a/src/CycloneDX.Core/Models/ServiceDataChoices.cs b/src/CycloneDX.Core/Models/ServiceDataChoices.cs index 51a64076..0ace05c2 100644 --- a/src/CycloneDX.Core/Models/ServiceDataChoices.cs +++ b/src/CycloneDX.Core/Models/ServiceDataChoices.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ServiceDataChoices : IXmlSerializable + public class ServiceDataChoices : BomEntity, IXmlSerializable { internal SpecificationVersion SpecVersion { get; set; } diff --git a/src/CycloneDX.Core/Models/Step.cs b/src/CycloneDX.Core/Models/Step.cs index 962c1e1e..b52a28dd 100644 --- a/src/CycloneDX.Core/Models/Step.cs +++ b/src/CycloneDX.Core/Models/Step.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("step")] [ProtoContract] - public class Step + public class Step : BomEntity { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ToolChoices.cs b/src/CycloneDX.Core/Models/ToolChoices.cs index e9059e57..3f56d952 100644 --- a/src/CycloneDX.Core/Models/ToolChoices.cs +++ b/src/CycloneDX.Core/Models/ToolChoices.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ToolChoices : IXmlSerializable + public class ToolChoices : BomEntity, IXmlSerializable { internal SpecificationVersion SpecVersion { get; set; } diff --git a/src/CycloneDX.Core/Models/Trigger.cs b/src/CycloneDX.Core/Models/Trigger.cs index 5b8407b2..2aa1bd88 100644 --- a/src/CycloneDX.Core/Models/Trigger.cs +++ b/src/CycloneDX.Core/Models/Trigger.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("trigger")] [ProtoContract] - public class Trigger + public class Trigger : BomEntity { [ProtoContract] public enum TriggerType @@ -42,7 +42,7 @@ public enum TriggerType } [ProtoContract] - public class Condition + public class Condition : BomEntity { [XmlElement("description")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Volume.cs b/src/CycloneDX.Core/Models/Volume.cs index 25a76eeb..d4ed0432 100644 --- a/src/CycloneDX.Core/Models/Volume.cs +++ b/src/CycloneDX.Core/Models/Volume.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("volume")] [ProtoContract] - public class Volume + public class Volume : BomEntity { [ProtoContract] public enum VolumeMode diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs b/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs index 9017d166..dea429c3 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/ProofOfConcept.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class ProofOfConcept + public class ProofOfConcept : BomEntity { [XmlElement("reproductionSteps")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Workflow.cs b/src/CycloneDX.Core/Models/Workflow.cs index b74a9cd2..ed52c85c 100644 --- a/src/CycloneDX.Core/Models/Workflow.cs +++ b/src/CycloneDX.Core/Models/Workflow.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workflow")] [ProtoContract] - public class Workflow + public class Workflow : BomEntity { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/WorkflowTask.cs b/src/CycloneDX.Core/Models/WorkflowTask.cs index 9eb0d514..bf2efeb6 100644 --- a/src/CycloneDX.Core/Models/WorkflowTask.cs +++ b/src/CycloneDX.Core/Models/WorkflowTask.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("task")] [ProtoContract] - public class WorkflowTask + public class WorkflowTask : BomEntity { [ProtoContract] public enum TaskType diff --git a/src/CycloneDX.Core/Models/Workspace.cs b/src/CycloneDX.Core/Models/Workspace.cs index ccc43f79..d9fac743 100644 --- a/src/CycloneDX.Core/Models/Workspace.cs +++ b/src/CycloneDX.Core/Models/Workspace.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workspace")] [ProtoContract] - public class Workspace + public class Workspace : BomEntity { [ProtoContract] public enum AccessModeType From 545c2188bbdcb59d242f34fcdb343bb6215b98e0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 22 Sep 2023 12:42:23 +0200 Subject: [PATCH 231/285] Bom: introduce GetBomRefsByContainer() and transposed GetBomRefsWithContainer() helpers, and RenameBomRef() feature Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 199 +++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 7ab34e1f..77499423 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -306,5 +306,204 @@ public void BomMetadataUpdate(bool generateNewSerialNumber) this.BomMetadataUpdateSerialNumberVersion(generateNewSerialNumber); this.BomMetadataUpdateTimestamp(); } + + /// + /// Provide a Dictionary whose keys are container BomEntities + /// and values are lists of one or more directly contained + /// entities with a BomRef attribute, e.g. the Bom itself and + /// the Components in it; or the Metadata and the Component + /// description in it; or certain Components or Tools with a + /// set of further "structural" components. + /// + /// The assumption per CycloneDX spec, not directly challenged + /// in this method, is that each such listed "contained entity" + /// (likely Component instances) has an unique BomRef value across + /// the whole single Bom document. Other Bom documents may however + /// have the same BomRef value (trivially "1", "2", ...) which + /// is attached to description of an unrelated entity. This can + /// impact such operations as a FlatMerge() of different Boms. + /// + /// See also: GetBomRefsWithContainer() with transposed returns. + /// + /// + public Dictionary> GetBomRefsByContainer() + { + Dictionary> dict = new Dictionary>(); + + // With CycloneDX spec 1.4 or older it might be feasible to + // walk specific properties of the Bom instance to look into + // their contents by known class types. As seen by excerpt + // from the spec below, just to list the locations where a + // "bom-ref" value can be set to identify an entity or where + // such value can be used to refer back to that entity, such + // approach is nearly infeasible starting with CDX 1.5 -- so + // use of reflection below is a more sustainable choice. + // + // Looking in schema definitions search for items that should + // be bom-refs (whether the attributes of certain entry types, + // or back-references from whoever uses them): + // * in "*.schema.json" search for "#/definitions/refType", or + // * in "*.xsd" search for "bom:refType" and its super-set for + // certain use-cases "bom:bomReferenceType" + // Since CDX spec 1.5 note there is also a "refLinkType" with + // same formal syntax as "refType" but different purpose -- + // to specify back-references (as separate from identifiers + // of new unique entries). Also do not confuse with bomLink, + // bomLinkDocumentType, and bomLinkElementType which refer to + // entities in OTHER Bom documents (or those Boms themselves). + // + // As of CDX spec 1.4+, a "bom-ref" attribute can be specified in: + // * (1.4, 1.5) component/"bom-ref" + // * (1.4, 1.5) service/"bom-ref" + // * (1.4, 1.5) vulnerability/"bom-ref" + // * (1.5) organizationalEntity/"bom-ref" + // * (1.5) organizationalContact/"bom-ref" + // * (1.5) license/"bom-ref" + // * (1.5) license/licenseChoice/...expression.../"bom-ref" + // * (1.5) componentEvidence/occurrences[]/"bom-ref" + // * (1.5) compositions/"bom-ref" + // * (1.5) annotations/"bom-ref" + // * (1.5) modelCard/"bom-ref" + // * (1.5) componentData/"bom-ref" + // * (1.5) formula/"bom-ref" + // * (1.5) workflow/"bom-ref" + // * (1.5) task/"bom-ref" + // * (1.5) workspace/"bom-ref" + // * (1.5) trigger/"bom-ref" + // and referred from: + // * dependency/"ref" => only "component" (1.4), or + // "component or service" (since 1.5) + // * dependency/"dependsOn[]" => only "component" (1.4), + // or "component or service" (since 1.5) + // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" + // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" + // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" + // * (1.5) componentEvidence/identity/tools[] => any, see spec + // * (1.5) annotations/subjects[] => any + // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") + // * (1.5) resourceReferenceChoice/"ref" => any + // + // Notably, CDX 1.5 also introduces resourceReferenceChoice + // which generalizes internal or external references, used in: + // * (1.5) workflow/resourceReferences[] + // * (1.5) task/resourceReferences[] + // * (1.5) workspace/resourceReferences[] + // * (1.5) trigger/resourceReferences[] + // * (1.5) event/{source,target} + // * (1.5) {inputType,outputType}/{source,target,resource} + // The CDX 1.5 tasks, workflows etc. also can reference each other. + // + // In particular, "component" instances (e.g. per JSON + // "#/definitions/component" spec search) can be direct + // properties (or property arrays) in: + // * (1.4, 1.5) component/pedigree/{ancestors,descendants,variants} + // * (1.4, 1.5) component/components[] -- structural hierarchy (not dependency tree) + // * (1.4, 1.5) bom/components[] + // * (1.4, 1.5) bom/metadata/component -- 0 or 1 item about the Bom itself + // * (1.5) bom/metadata/tools/components[] -- SW and HW tools used to create the Bom + // * (1.5) vulnerability/tools/components[] -- SW and HW tools used to describe the vuln + // * (1.5) formula/components[] + // + // Note that there may be potentially any level of nesting of + // components in components, and compositions, among other things. + // + // And "service" instances (per JSON "#/definitions/service"): + // * (1.4, 1.5) service/services[] + // * (1.4, 1.5) bom/services[] + // * (1.5) bom/metadata/tools/services[] -- services as tools used to create the Bom + // * (1.5) vulnerability/tools/services[] -- services as tools used to describe the vuln + // * (1.5) formula/services[] + // + // The CDX spec 1.5 also introduces "annotation" which can refer to + // such bom-ref carriers as service, component, organizationalEntity, + // organizationalContact. + + return dict; + } + + /// + /// Provide a Dictionary whose keys are "contained" entities + /// with a BomRef attribute and values are their direct + /// container BomEntities, e.g. each Bom.Components[] list + /// entry referring the Bom itself; or the Metadata.Component + /// entry referring the Metadata; or further "structural" + /// components in certain Component or Tool entities. + /// + /// The assumption per CycloneDX spec, not directly challenged + /// in this method, is that each such listed "contained entity" + /// (likely Component instances) has an unique BomRef value across + /// the whole single Bom document. Other Bom documents may however + /// have the same BomRef value (trivially "1", "2", ...) which + /// is attached to description of an unrelated entity. This can + /// impact such operations as a FlatMerge() of different Boms. + /// + /// See also: GetBomRefsByContainer() with transposed returns. + /// + /// + public Dictionary GetBomRefsWithContainer() + { + Dictionary> dictByC = this.GetBomRefsByContainer(); + Dictionary dictWithC = new Dictionary(); + + foreach (var (container, listItems) in dictByC) + { + if (listItems is null || container is null || listItems.Count < 1) { + continue; + } + + foreach (var item in listItems) { + dictWithC[item] = container; + } + } + + return dictWithC; + } + + /// + /// Rename all occurrences of the "BomRef" (its value definition + /// to name an entity, if present in this Bom document, and the + /// references to it from other entities). + /// + /// This version of the method considers a cache of information + /// about current BomEntity relationships in this document, as + /// prepared by an earlier call to GetBomRefsWithContainer() and + /// cached by caller (may speed up the loops in case of massive + /// processing). + /// + /// Old value of BomRef + /// New value of BomRef + /// Cached output of earlier GetBomRefsWithContainer(); + /// contents of the cache can change due to successful renaming + /// to keep reflecting BomEntity relations in this document. + /// + /// + /// False if had no hits, had collisions, etc.; + /// True if renamed something without any errors. + /// + /// TODO: throw Exceptions instead of False, + /// to help callers discern the error cases? + /// + public bool RenameBomRef(string oldRef, string newRef, Dictionary dict) + { + return false; + } + + /// + /// See related method + /// RenameBomRef(string oldRef, string newRef, Dictionary dict) + /// for details. + /// + /// This version of the method prepares and discards the helper + /// dictionary with mapping of cross-referencing entities, and + /// is easier to use in code for single-use cases but is less + /// efficient for massive processing loops. + /// + /// Old value of BomRef + /// New value of BomRef + /// False if had no hits; True if renamed something without any errors + public bool RenameBomRef(string oldRef, string newRef) + { + return this.RenameBomRef(oldRef, newRef, this.GetBomRefsWithContainer()); + } } } From 3a10bf9e05ee7e10246d2006994dfaaa98ae7907 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 22 Sep 2023 08:56:04 +0200 Subject: [PATCH 232/285] BomEntity.cs: Introduce BomWalkResult helper class Walk a BomEntity (typically whole document) and store the discovered results for merge, validation, etc. purposes. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 200 +++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0e832f96..2a6f51f2 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -18,10 +18,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text.Json.Serialization; +using System.Xml; namespace CycloneDX.Models { @@ -1117,4 +1119,202 @@ public bool MergeWith(BomEntity obj, BomEntityListMergeHelperStrategy listMergeH this.GetType()); } } + + /// + /// Helper class for Bom.GetBomRefsInContainers() et al discovery tracking. + /// + public class BomWalkResult + { + /// + /// The BomEntity (normally a whole Bom document) + /// which was walked and reported here. + /// + public BomEntity bomRoot = null; + + /// + /// Populated by GetBomRefsInContainers(), + /// keys are "container" entities and values + /// are lists of "contained" entities which + /// have a BomRef or equivalent property. + /// + readonly public Dictionary> dictRefsInContainers = new Dictionary>(); + + /// + /// Populated by GetBomRefsInContainers(), + /// keys are "Ref" or equivalent string values + /// which link back to a "BomRef" hopefully + /// defined somewhere in the same Bom document + /// (but may be dangling, or sometimes co-opted + /// with external links to other Bom documents!), + /// and values are lists of entities which use + /// this same "ref" value. + /// + readonly public Dictionary> dictBackrefs = new Dictionary>(); + + public void reset() + { + dictRefsInContainers.Clear(); + dictBackrefs.Clear(); + + bomRoot = null; + } + + public void reset(BomEntity newRoot) + { + this.reset(); + this.bomRoot = newRoot; + } + + /// + /// Provide a Dictionary whose keys are container BomEntities + /// and values are lists of one or more directly contained + /// entities with a BomRef attribute, e.g. the Bom itself and + /// the Components in it; or the Metadata and the Component + /// description in it; or certain Components or Tools with a + /// set of further "structural" components. + /// + /// The assumption per CycloneDX spec, not directly challenged + /// in this method, is that each such listed "contained entity" + /// (likely Component instances) has an unique BomRef value across + /// the whole single Bom document. Other Bom documents may however + /// have the same BomRef value (trivially "1", "2", ...) which + /// is attached to description of an unrelated entity. This can + /// impact such operations as a FlatMerge() of different Boms. + /// + /// See also: GetBomRefsWithContainer() with transposed returns. + /// + /// + public Dictionary> GetBomRefsByContainer() + { + Dictionary> dict = new Dictionary>(); + + // With CycloneDX spec 1.4 or older it might be feasible to + // walk specific properties of the Bom instance to look into + // their contents by known class types. As seen by excerpt + // from the spec below, just to list the locations where a + // "bom-ref" value can be set to identify an entity or where + // such value can be used to refer back to that entity, such + // approach is nearly infeasible starting with CDX 1.5 -- so + // use of reflection below is a more sustainable choice. + // + // Looking in schema definitions search for items that should + // be bom-refs (whether the attributes of certain entry types, + // or back-references from whoever uses them): + // * in "*.schema.json" search for "#/definitions/refType", or + // * in "*.xsd" search for "bom:refType" and its super-set for + // certain use-cases "bom:bomReferenceType" + // Since CDX spec 1.5 note there is also a "refLinkType" with + // same formal syntax as "refType" but different purpose -- + // to specify back-references (as separate from identifiers + // of new unique entries). Also do not confuse with bomLink, + // bomLinkDocumentType, and bomLinkElementType which refer to + // entities in OTHER Bom documents (or those Boms themselves). + // + // As of CDX spec 1.4+, a "bom-ref" attribute can be specified in: + // * (1.4, 1.5) component/"bom-ref" + // * (1.4, 1.5) service/"bom-ref" + // * (1.4, 1.5) vulnerability/"bom-ref" + // * (1.5) organizationalEntity/"bom-ref" + // * (1.5) organizationalContact/"bom-ref" + // * (1.5) license/"bom-ref" + // * (1.5) license/licenseChoice/...expression.../"bom-ref" + // * (1.5) componentEvidence/occurrences[]/"bom-ref" + // * (1.5) compositions/"bom-ref" + // * (1.5) annotations/"bom-ref" + // * (1.5) modelCard/"bom-ref" + // * (1.5) componentData/"bom-ref" + // * (1.5) formula/"bom-ref" + // * (1.5) workflow/"bom-ref" + // * (1.5) task/"bom-ref" + // * (1.5) workspace/"bom-ref" + // * (1.5) trigger/"bom-ref" + // and referred from: + // * dependency/"ref" => only "component" (1.4), or + // "component or service" (since 1.5) + // * dependency/"dependsOn[]" => only "component" (1.4), + // or "component or service" (since 1.5) + // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" + // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" + // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" + // * (1.5) componentEvidence/identity/tools[] => any, see spec + // * (1.5) annotations/subjects[] => any + // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") + // * (1.5) resourceReferenceChoice/"ref" => any + // + // Notably, CDX 1.5 also introduces resourceReferenceChoice + // which generalizes internal or external references, used in: + // * (1.5) workflow/resourceReferences[] + // * (1.5) task/resourceReferences[] + // * (1.5) workspace/resourceReferences[] + // * (1.5) trigger/resourceReferences[] + // * (1.5) event/{source,target} + // * (1.5) {inputType,outputType}/{source,target,resource} + // The CDX 1.5 tasks, workflows etc. also can reference each other. + // + // In particular, "component" instances (e.g. per JSON + // "#/definitions/component" spec search) can be direct + // properties (or property arrays) in: + // * (1.4, 1.5) component/pedigree/{ancestors,descendants,variants} + // * (1.4, 1.5) component/components[] -- structural hierarchy (not dependency tree) + // * (1.4, 1.5) bom/components[] + // * (1.4, 1.5) bom/metadata/component -- 0 or 1 item about the Bom itself + // * (1.5) bom/metadata/tools/components[] -- SW and HW tools used to create the Bom + // * (1.5) vulnerability/tools/components[] -- SW and HW tools used to describe the vuln + // * (1.5) formula/components[] + // + // Note that there may be potentially any level of nesting of + // components in components, and compositions, among other things. + // + // And "service" instances (per JSON "#/definitions/service"): + // * (1.4, 1.5) service/services[] + // * (1.4, 1.5) bom/services[] + // * (1.5) bom/metadata/tools/services[] -- services as tools used to create the Bom + // * (1.5) vulnerability/tools/services[] -- services as tools used to describe the vuln + // * (1.5) formula/services[] + // + // The CDX spec 1.5 also introduces "annotation" which can refer to + // such bom-ref carriers as service, component, organizationalEntity, + // organizationalContact. + + return dict; + } + + /// + /// Provide a Dictionary whose keys are "contained" entities + /// with a BomRef attribute and values are their direct + /// container BomEntities, e.g. each Bom.Components[] list + /// entry referring the Bom itself; or the Metadata.Component + /// entry referring the Metadata; or further "structural" + /// components in certain Component or Tool entities. + /// + /// The assumption per CycloneDX spec, not directly challenged + /// in this method, is that each such listed "contained entity" + /// (likely Component instances) has an unique BomRef value across + /// the whole single Bom document. Other Bom documents may however + /// have the same BomRef value (trivially "1", "2", ...) which + /// is attached to description of an unrelated entity. This can + /// impact such operations as a FlatMerge() of different Boms. + /// + /// See also: GetBomRefsByContainer() with transposed returns. + /// + /// + public Dictionary GetBomRefsWithContainer() + { + Dictionary> dictByC = this.GetBomRefsByContainer(); + Dictionary dictWithC = new Dictionary(); + + foreach (var (container, listItems) in dictByC) + { + if (listItems is null || container is null || listItems.Count < 1) { + continue; + } + + foreach (var item in listItems) { + dictWithC[item] = container; + } + } + + return dictWithC; + } + } } From 26406343e3455ad62c824db273d81add77541562 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 29 Sep 2023 14:24:53 +0200 Subject: [PATCH 233/285] Bom.cs: moved most of BomRef discovery PoC/logic into a BomWalkResult ...using the helper class to reduce reference tossing and to help keep track of related data collections Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 165 +++++++++++-------------------- 1 file changed, 57 insertions(+), 108 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 77499423..22061002 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -307,6 +307,24 @@ public void BomMetadataUpdate(bool generateNewSerialNumber) this.BomMetadataUpdateTimestamp(); } + /// + /// Prepare a BomWalkResult discovery report starting from + /// this Bom document. Callers can cache it to re-use for + /// repetitive operations. + /// + /// + public BomWalkResult WalkThis() + { + BomWalkResult res = new BomWalkResult(); + res.reset(this); + + // Note: passing "container=null" should be safe here, as + // long as this Bom type does not have a BomRef property. + res.SerializeBomEntity_BomRefs(this, null); + + return res; + } + /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained @@ -326,99 +344,26 @@ public void BomMetadataUpdate(bool generateNewSerialNumber) /// See also: GetBomRefsWithContainer() with transposed returns. /// /// - public Dictionary> GetBomRefsByContainer() + public Dictionary> GetBomRefsInContainers(BomWalkResult res) { - Dictionary> dict = new Dictionary>(); - - // With CycloneDX spec 1.4 or older it might be feasible to - // walk specific properties of the Bom instance to look into - // their contents by known class types. As seen by excerpt - // from the spec below, just to list the locations where a - // "bom-ref" value can be set to identify an entity or where - // such value can be used to refer back to that entity, such - // approach is nearly infeasible starting with CDX 1.5 -- so - // use of reflection below is a more sustainable choice. - // - // Looking in schema definitions search for items that should - // be bom-refs (whether the attributes of certain entry types, - // or back-references from whoever uses them): - // * in "*.schema.json" search for "#/definitions/refType", or - // * in "*.xsd" search for "bom:refType" and its super-set for - // certain use-cases "bom:bomReferenceType" - // Since CDX spec 1.5 note there is also a "refLinkType" with - // same formal syntax as "refType" but different purpose -- - // to specify back-references (as separate from identifiers - // of new unique entries). Also do not confuse with bomLink, - // bomLinkDocumentType, and bomLinkElementType which refer to - // entities in OTHER Bom documents (or those Boms themselves). - // - // As of CDX spec 1.4+, a "bom-ref" attribute can be specified in: - // * (1.4, 1.5) component/"bom-ref" - // * (1.4, 1.5) service/"bom-ref" - // * (1.4, 1.5) vulnerability/"bom-ref" - // * (1.5) organizationalEntity/"bom-ref" - // * (1.5) organizationalContact/"bom-ref" - // * (1.5) license/"bom-ref" - // * (1.5) license/licenseChoice/...expression.../"bom-ref" - // * (1.5) componentEvidence/occurrences[]/"bom-ref" - // * (1.5) compositions/"bom-ref" - // * (1.5) annotations/"bom-ref" - // * (1.5) modelCard/"bom-ref" - // * (1.5) componentData/"bom-ref" - // * (1.5) formula/"bom-ref" - // * (1.5) workflow/"bom-ref" - // * (1.5) task/"bom-ref" - // * (1.5) workspace/"bom-ref" - // * (1.5) trigger/"bom-ref" - // and referred from: - // * dependency/"ref" => only "component" (1.4), or - // "component or service" (since 1.5) - // * dependency/"dependsOn[]" => only "component" (1.4), - // or "component or service" (since 1.5) - // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" - // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" - // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" - // * (1.5) componentEvidence/identity/tools[] => any, see spec - // * (1.5) annotations/subjects[] => any - // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") - // * (1.5) resourceReferenceChoice/"ref" => any - // - // Notably, CDX 1.5 also introduces resourceReferenceChoice - // which generalizes internal or external references, used in: - // * (1.5) workflow/resourceReferences[] - // * (1.5) task/resourceReferences[] - // * (1.5) workspace/resourceReferences[] - // * (1.5) trigger/resourceReferences[] - // * (1.5) event/{source,target} - // * (1.5) {inputType,outputType}/{source,target,resource} - // The CDX 1.5 tasks, workflows etc. also can reference each other. - // - // In particular, "component" instances (e.g. per JSON - // "#/definitions/component" spec search) can be direct - // properties (or property arrays) in: - // * (1.4, 1.5) component/pedigree/{ancestors,descendants,variants} - // * (1.4, 1.5) component/components[] -- structural hierarchy (not dependency tree) - // * (1.4, 1.5) bom/components[] - // * (1.4, 1.5) bom/metadata/component -- 0 or 1 item about the Bom itself - // * (1.5) bom/metadata/tools/components[] -- SW and HW tools used to create the Bom - // * (1.5) vulnerability/tools/components[] -- SW and HW tools used to describe the vuln - // * (1.5) formula/components[] - // - // Note that there may be potentially any level of nesting of - // components in components, and compositions, among other things. - // - // And "service" instances (per JSON "#/definitions/service"): - // * (1.4, 1.5) service/services[] - // * (1.4, 1.5) bom/services[] - // * (1.5) bom/metadata/tools/services[] -- services as tools used to create the Bom - // * (1.5) vulnerability/tools/services[] -- services as tools used to describe the vuln - // * (1.5) formula/services[] - // - // The CDX spec 1.5 also introduces "annotation" which can refer to - // such bom-ref carriers as service, component, organizationalEntity, - // organizationalContact. - - return dict; + if (res.bomRoot != this) + { + // throw? + return null; + } + return res.dictRefsInContainers; + } + + /// + /// This is a run-once method to get a dictionary. + /// See GetBomRefsInContainers(BomWalkResult) for one using a cache + /// prepared by WalkThis() for mass manipulations. + /// + /// + public Dictionary> GetBomRefsInContainers() + { + BomWalkResult res = WalkThis(); + return GetBomRefsInContainers(res); } /// @@ -437,26 +382,29 @@ public Dictionary> GetBomRefsByContainer() /// is attached to description of an unrelated entity. This can /// impact such operations as a FlatMerge() of different Boms. /// - /// See also: GetBomRefsByContainer() with transposed returns. + /// See also: GetBomRefsInContainers() with transposed returns. /// /// - public Dictionary GetBomRefsWithContainer() + public Dictionary GetBomRefsWithContainer(BomWalkResult res) { - Dictionary> dictByC = this.GetBomRefsByContainer(); - Dictionary dictWithC = new Dictionary(); - - foreach (var (container, listItems) in dictByC) + if (res.bomRoot != this) { - if (listItems is null || container is null || listItems.Count < 1) { - continue; - } - - foreach (var item in listItems) { - dictWithC[item] = container; - } + // throw? + return null; } + return res.GetBomRefsWithContainer(); + } - return dictWithC; + /// + /// This is a run-once method to get a dictionary. + /// See GetBomRefsWithContainer(BomWalkResult) for one using a cache + /// prepared by WalkThis() for mass manipulations. + /// + /// + public Dictionary GetBomRefsWithContainer() + { + BomWalkResult res = WalkThis(); + return res.GetBomRefsWithContainer(); } /// @@ -483,7 +431,7 @@ public Dictionary GetBomRefsWithContainer() /// TODO: throw Exceptions instead of False, /// to help callers discern the error cases? /// - public bool RenameBomRef(string oldRef, string newRef, Dictionary dict) + public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) { return false; } @@ -503,7 +451,8 @@ public bool RenameBomRef(string oldRef, string newRef, DictionaryFalse if had no hits; True if renamed something without any errors public bool RenameBomRef(string oldRef, string newRef) { - return this.RenameBomRef(oldRef, newRef, this.GetBomRefsWithContainer()); + BomWalkResult res = WalkThis(); + return this.RenameBomRef(oldRef, newRef, res); } } } From 82bef904f553fdbc237ddb47501283483151a6b9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 25 Sep 2023 23:16:19 +0200 Subject: [PATCH 234/285] BomWalkResult: add SerializeBomEntity_BomRefs() helper ...to iterate the actual search for bom-refs Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 143 +++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 2a6f51f2..f41b76a9 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1165,6 +1165,143 @@ public void reset(BomEntity newRoot) this.bomRoot = newRoot; } + /// + /// Helper for Bom.GetBomRefsByContainer(). + /// + /// A BomEntity instance currently being investigated + /// A BomEntity instance whose attribute + /// (or member of a List<> attribute) is currently being + /// investigated. May be null when starting iteration + /// from this.GetBomRefsByContainer() method. + /// Keys are "container" BomEntities, + /// and values are the lists of "directly contained" + /// BomEntities which have a BomRef attribute. + private void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container, ref Dictionary> dict) + { + Type type = obj.GetType(); + + // Sanity-check: we do not recurse into non-BomEntity types. + // Hopefully the compiler or runtime would not have let other obj's in... + if (type is null || (!(typeof(BomEntity).IsAssignableFrom(type)))) + { + return; + } + + foreach (PropertyInfo propInfo in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + // We do not recurse into non-BomEntity types + if (propInfo is null) + { + // Is this expected? Maybe throw? + continue; + } + + Type propType = propInfo.PropertyType; + + // If the type of current "obj" contains a "bom-ref", or + // has annotations like [JsonPropertyName("bom-ref")] and + // [XmlAttribute("bom-ref")], save it into the dictionary. + + // TODO: Pedantically it would be better to either parse + // and consult corresponding CycloneDX spec, somehow, for + // properties which have needed schema-defined type (see + // detailed comments in GetBomRefsByContainer() method). + if ( + (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef") + || (Array.Find(propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true), x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null) + || (Array.Find(propInfo.GetCustomAttributes(typeof(XmlAttribute), true), x => ((XmlAttribute)x).Name == "bom-ref") != null) + ) + { + if (!(dict.ContainsKey(container))) + { + dict[container] = new List(); + } + + dict[container].Add((BomEntity)obj); + + // Done with this string property, look at next + continue; + } + + // We do not recurse into non-BomEntity types + bool propIsListBomEntity = ( + (propType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(System.Collections.IList))) + && (Array.Find(propType.GetTypeInfo().GenericTypeArguments, + x => typeof(BomEntity).GetTypeInfo().IsAssignableFrom(x.GetTypeInfo())) != null) + ); + + if (!( + propIsListBomEntity + || (typeof(BomEntity).GetTypeInfo().IsAssignableFrom(propType.GetTypeInfo())) + )) + { + // Not a BomEntity or (potentially) a List of those + continue; + } + + var propVal = propInfo.GetValue(obj, null); + if (propVal is null) + { + continue; + } + + if (propIsListBomEntity) + { + // Use cached info where available + PropertyInfo listPropCount = null; + MethodInfo listMethodGetItem = null; + MethodInfo listMethodAdd = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(propType, out BomEntityListReflection refInfo)) + { + listPropCount = refInfo.propCount; + listMethodGetItem = refInfo.methodGetItem; + listMethodAdd = refInfo.methodAdd; + } + else + { + // No cached info about BomEntityListReflection[{propType} + listPropCount = propType.GetProperty("Count"); + listMethodGetItem = propType.GetMethod("get_Item"); + listMethodAdd = propType.GetMethod("Add"); + } + + if (listMethodGetItem == null || listPropCount == null || listMethodAdd == null) + { + // Should not have happened, but... + continue; + } + + int propValCount = (int)listPropCount.GetValue(propVal, null); + if (propValCount < 1) + { + // Empty list + continue; + } + + for (int o = 0; o < propValCount; o++) + { + var listVal = listMethodGetItem.Invoke(propVal, new object[] { o }); + if (listVal is null) + { + continue; + } + + if (!(listVal is BomEntity)) + { + break; + } + + SerializeBomEntity_BomRefs((BomEntity)listVal, obj, ref dict); + } + + // End of list, or a break per above + continue; + } + + SerializeBomEntity_BomRefs((BomEntity)propVal, obj, ref dict); + } + } + /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained @@ -1196,6 +1333,12 @@ public Dictionary> GetBomRefsByContainer() // such value can be used to refer back to that entity, such // approach is nearly infeasible starting with CDX 1.5 -- so // use of reflection below is a more sustainable choice. + + // Note: passing "container=null" should be safe here, as + // long as this Bom type does not have a BomRef property. + SerializeBomEntity_BomRefs(this, null, ref dict); + + // TL:DR further details: // // Looking in schema definitions search for items that should // be bom-refs (whether the attributes of certain entry types, From 86231b3acc7846c912253d77941d585f6601a247 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 16:38:32 +0200 Subject: [PATCH 235/285] BomWalkResult: move big comments to SerializeBomEntity_BomRefs() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 179 ++++++++++++------------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index f41b76a9..17621611 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1178,6 +1178,95 @@ public void reset(BomEntity newRoot) /// BomEntities which have a BomRef attribute. private void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container, ref Dictionary> dict) { + // With CycloneDX spec 1.4 or older it might be feasible to + // walk specific properties of the Bom instance to look into + // their contents by known class types. As seen by excerpt + // from the spec below, just to list the locations where a + // "bom-ref" value can be set to identify an entity or where + // such value can be used to refer back to that entity, such + // approach is nearly infeasible starting with CDX 1.5 -- so + // use of reflection below is a more sustainable choice. + + // TL:DR further details: + // + // Looking in schema definitions search for items that should + // be bom-refs (whether the attributes of certain entry types, + // or back-references from whoever uses them): + // * in "*.schema.json" search for "#/definitions/refType", or + // * in "*.xsd" search for "bom:refType" and its super-set for + // certain use-cases "bom:bomReferenceType" + // Since CDX spec 1.5 note there is also a "refLinkType" with + // same formal syntax as "refType" but different purpose -- + // to specify back-references (as separate from identifiers + // of new unique entries). Also do not confuse with bomLink, + // bomLinkDocumentType, and bomLinkElementType which refer to + // entities in OTHER Bom documents (or those Boms themselves). + // + // As of CDX spec 1.4+, a "bom-ref" attribute can be specified in: + // * (1.4, 1.5) component/"bom-ref" + // * (1.4, 1.5) service/"bom-ref" + // * (1.4, 1.5) vulnerability/"bom-ref" + // * (1.5) organizationalEntity/"bom-ref" + // * (1.5) organizationalContact/"bom-ref" + // * (1.5) license/"bom-ref" + // * (1.5) license/licenseChoice/...expression.../"bom-ref" + // * (1.5) componentEvidence/occurrences[]/"bom-ref" + // * (1.5) compositions/"bom-ref" + // * (1.5) annotations/"bom-ref" + // * (1.5) modelCard/"bom-ref" + // * (1.5) componentData/"bom-ref" + // * (1.5) formula/"bom-ref" + // * (1.5) workflow/"bom-ref" + // * (1.5) task/"bom-ref" + // * (1.5) workspace/"bom-ref" + // * (1.5) trigger/"bom-ref" + // and referred from: + // * dependency/"ref" => only "component" (1.4), or + // "component or service" (since 1.5) + // * dependency/"dependsOn[]" => only "component" (1.4), + // or "component or service" (since 1.5) + // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" + // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" + // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" + // * (1.5) componentEvidence/identity/tools[] => any, see spec + // * (1.5) annotations/subjects[] => any + // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") + // * (1.5) resourceReferenceChoice/"ref" => any + // + // Notably, CDX 1.5 also introduces resourceReferenceChoice + // which generalizes internal or external references, used in: + // * (1.5) workflow/resourceReferences[] + // * (1.5) task/resourceReferences[] + // * (1.5) workspace/resourceReferences[] + // * (1.5) trigger/resourceReferences[] + // * (1.5) event/{source,target} + // * (1.5) {inputType,outputType}/{source,target,resource} + // The CDX 1.5 tasks, workflows etc. also can reference each other. + // + // In particular, "component" instances (e.g. per JSON + // "#/definitions/component" spec search) can be direct + // properties (or property arrays) in: + // * (1.4, 1.5) component/pedigree/{ancestors,descendants,variants} + // * (1.4, 1.5) component/components[] -- structural hierarchy (not dependency tree) + // * (1.4, 1.5) bom/components[] + // * (1.4, 1.5) bom/metadata/component -- 0 or 1 item about the Bom itself + // * (1.5) bom/metadata/tools/components[] -- SW and HW tools used to create the Bom + // * (1.5) vulnerability/tools/components[] -- SW and HW tools used to describe the vuln + // * (1.5) formula/components[] + // + // Note that there may be potentially any level of nesting of + // components in components, and compositions, among other things. + // + // And "service" instances (per JSON "#/definitions/service"): + // * (1.4, 1.5) service/services[] + // * (1.4, 1.5) bom/services[] + // * (1.5) bom/metadata/tools/services[] -- services as tools used to create the Bom + // * (1.5) vulnerability/tools/services[] -- services as tools used to describe the vuln + // * (1.5) formula/services[] + // + // The CDX spec 1.5 also introduces "annotation" which can refer to + // such bom-ref carriers as service, component, organizationalEntity, + // organizationalContact. Type type = obj.GetType(); // Sanity-check: we do not recurse into non-BomEntity types. @@ -1325,100 +1414,10 @@ public Dictionary> GetBomRefsByContainer() { Dictionary> dict = new Dictionary>(); - // With CycloneDX spec 1.4 or older it might be feasible to - // walk specific properties of the Bom instance to look into - // their contents by known class types. As seen by excerpt - // from the spec below, just to list the locations where a - // "bom-ref" value can be set to identify an entity or where - // such value can be used to refer back to that entity, such - // approach is nearly infeasible starting with CDX 1.5 -- so - // use of reflection below is a more sustainable choice. - // Note: passing "container=null" should be safe here, as // long as this Bom type does not have a BomRef property. SerializeBomEntity_BomRefs(this, null, ref dict); - // TL:DR further details: - // - // Looking in schema definitions search for items that should - // be bom-refs (whether the attributes of certain entry types, - // or back-references from whoever uses them): - // * in "*.schema.json" search for "#/definitions/refType", or - // * in "*.xsd" search for "bom:refType" and its super-set for - // certain use-cases "bom:bomReferenceType" - // Since CDX spec 1.5 note there is also a "refLinkType" with - // same formal syntax as "refType" but different purpose -- - // to specify back-references (as separate from identifiers - // of new unique entries). Also do not confuse with bomLink, - // bomLinkDocumentType, and bomLinkElementType which refer to - // entities in OTHER Bom documents (or those Boms themselves). - // - // As of CDX spec 1.4+, a "bom-ref" attribute can be specified in: - // * (1.4, 1.5) component/"bom-ref" - // * (1.4, 1.5) service/"bom-ref" - // * (1.4, 1.5) vulnerability/"bom-ref" - // * (1.5) organizationalEntity/"bom-ref" - // * (1.5) organizationalContact/"bom-ref" - // * (1.5) license/"bom-ref" - // * (1.5) license/licenseChoice/...expression.../"bom-ref" - // * (1.5) componentEvidence/occurrences[]/"bom-ref" - // * (1.5) compositions/"bom-ref" - // * (1.5) annotations/"bom-ref" - // * (1.5) modelCard/"bom-ref" - // * (1.5) componentData/"bom-ref" - // * (1.5) formula/"bom-ref" - // * (1.5) workflow/"bom-ref" - // * (1.5) task/"bom-ref" - // * (1.5) workspace/"bom-ref" - // * (1.5) trigger/"bom-ref" - // and referred from: - // * dependency/"ref" => only "component" (1.4), or - // "component or service" (since 1.5) - // * dependency/"dependsOn[]" => only "component" (1.4), - // or "component or service" (since 1.5) - // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" - // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" - // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" - // * (1.5) componentEvidence/identity/tools[] => any, see spec - // * (1.5) annotations/subjects[] => any - // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") - // * (1.5) resourceReferenceChoice/"ref" => any - // - // Notably, CDX 1.5 also introduces resourceReferenceChoice - // which generalizes internal or external references, used in: - // * (1.5) workflow/resourceReferences[] - // * (1.5) task/resourceReferences[] - // * (1.5) workspace/resourceReferences[] - // * (1.5) trigger/resourceReferences[] - // * (1.5) event/{source,target} - // * (1.5) {inputType,outputType}/{source,target,resource} - // The CDX 1.5 tasks, workflows etc. also can reference each other. - // - // In particular, "component" instances (e.g. per JSON - // "#/definitions/component" spec search) can be direct - // properties (or property arrays) in: - // * (1.4, 1.5) component/pedigree/{ancestors,descendants,variants} - // * (1.4, 1.5) component/components[] -- structural hierarchy (not dependency tree) - // * (1.4, 1.5) bom/components[] - // * (1.4, 1.5) bom/metadata/component -- 0 or 1 item about the Bom itself - // * (1.5) bom/metadata/tools/components[] -- SW and HW tools used to create the Bom - // * (1.5) vulnerability/tools/components[] -- SW and HW tools used to describe the vuln - // * (1.5) formula/components[] - // - // Note that there may be potentially any level of nesting of - // components in components, and compositions, among other things. - // - // And "service" instances (per JSON "#/definitions/service"): - // * (1.4, 1.5) service/services[] - // * (1.4, 1.5) bom/services[] - // * (1.5) bom/metadata/tools/services[] -- services as tools used to create the Bom - // * (1.5) vulnerability/tools/services[] -- services as tools used to describe the vuln - // * (1.5) formula/services[] - // - // The CDX spec 1.5 also introduces "annotation" which can refer to - // such bom-ref carriers as service, component, organizationalEntity, - // organizationalContact. - return dict; } From e950f088ff018e4d521974683d07f668fc04e8d3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 16:38:32 +0200 Subject: [PATCH 236/285] BomWalkResult: use class props for results, not PoC dicts Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 17621611..e9f3c6d3 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1173,10 +1173,7 @@ public void reset(BomEntity newRoot) /// (or member of a List<> attribute) is currently being /// investigated. May be null when starting iteration /// from this.GetBomRefsByContainer() method. - /// Keys are "container" BomEntities, - /// and values are the lists of "directly contained" - /// BomEntities which have a BomRef attribute. - private void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container, ref Dictionary> dict) + public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) { // With CycloneDX spec 1.4 or older it might be feasible to // walk specific properties of the Bom instance to look into @@ -1412,13 +1409,7 @@ private void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container, ref /// public Dictionary> GetBomRefsByContainer() { - Dictionary> dict = new Dictionary>(); - - // Note: passing "container=null" should be safe here, as - // long as this Bom type does not have a BomRef property. - SerializeBomEntity_BomRefs(this, null, ref dict); - - return dict; + return dictRefsInContainers; } /// @@ -1442,10 +1433,9 @@ public Dictionary> GetBomRefsByContainer() /// public Dictionary GetBomRefsWithContainer() { - Dictionary> dictByC = this.GetBomRefsByContainer(); Dictionary dictWithC = new Dictionary(); - foreach (var (container, listItems) in dictByC) + foreach (var (container, listItems) in dictRefsInContainers) { if (listItems is null || container is null || listItems.Count < 1) { continue; From 1d3232b7dc5601baa220c0f42f2bbed0d64a588b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 00:54:03 +0200 Subject: [PATCH 237/285] BomWalkResult: SerializeBomEntity_BomRefs(): try/catch GetValue() ...to rule out null items earlier in the loop Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e9f3c6d3..e9d7ca77 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1282,6 +1282,25 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } + object propVal = null; + try + { + propVal = propInfo.GetValue(obj, null); + } + catch (TargetInvocationException) + { + propVal = null; + } + catch (InvalidOperationException) + { + propVal = null; + } + + if (propVal is null) + { + continue; + } + Type propType = propInfo.PropertyType; // If the type of current "obj" contains a "bom-ref", or @@ -1325,12 +1344,6 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } - var propVal = propInfo.GetValue(obj, null); - if (propVal is null) - { - continue; - } - if (propIsListBomEntity) { // Use cached info where available From cc59a9d1f038a0c263930bbe0a0d183cee51a210 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 01:38:15 +0200 Subject: [PATCH 238/285] BomWalkResult: SerializeBomEntity_BomRefs(): fix naming for objType Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e9d7ca77..65abd9e2 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1264,16 +1264,21 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // The CDX spec 1.5 also introduces "annotation" which can refer to // such bom-ref carriers as service, component, organizationalEntity, // organizationalContact. - Type type = obj.GetType(); + if (obj is null) + { + return; + } + + Type objType = obj.GetType(); // Sanity-check: we do not recurse into non-BomEntity types. // Hopefully the compiler or runtime would not have let other obj's in... - if (type is null || (!(typeof(BomEntity).IsAssignableFrom(type)))) + if (objType is null || (!(typeof(BomEntity).IsAssignableFrom(objType)))) { return; } - foreach (PropertyInfo propInfo in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + foreach (PropertyInfo propInfo in objType.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { // We do not recurse into non-BomEntity types if (propInfo is null) From 270f768af7829099f93d7183f8d10af80045ffee Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 01:52:42 +0200 Subject: [PATCH 239/285] BomWalkResult: SerializeBomEntity_BomRefs(): check for "NonNullable*" prop names ...to avoid exception hiccups in debugger Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 65abd9e2..24c20d8e 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1287,9 +1287,21 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } + Type propType = propInfo.PropertyType; + object propVal = null; try { + if (propInfo.Name.StartsWith("NonNullable")) { + // It is a getter/setter-wrapped facade + // of a Nullable for some T - skip, + // we would inspect the raw item instead + // (factual nulls cause an exception and + // try/catch overhead here). + // FIXME: Is there an attribute for this, + // to avoid a string comparison in a loop? + continue; + } propVal = propInfo.GetValue(obj, null); } catch (TargetInvocationException) @@ -1306,8 +1318,6 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } - Type propType = propInfo.PropertyType; - // If the type of current "obj" contains a "bom-ref", or // has annotations like [JsonPropertyName("bom-ref")] and // [XmlAttribute("bom-ref")], save it into the dictionary. From 8ee6a67e4b1a073c602c72f82fe9f6db91d84c75 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 01:53:57 +0200 Subject: [PATCH 240/285] BomWalkResult: SerializeBomEntity_BomRefs(): with the name check for "NonNullable*", try/catch overhead is not needed anymore Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 32 ++++++++------------------ 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 24c20d8e..7e1c41cc 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1289,29 +1289,17 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) Type propType = propInfo.PropertyType; - object propVal = null; - try - { - if (propInfo.Name.StartsWith("NonNullable")) { - // It is a getter/setter-wrapped facade - // of a Nullable for some T - skip, - // we would inspect the raw item instead - // (factual nulls cause an exception and - // try/catch overhead here). - // FIXME: Is there an attribute for this, - // to avoid a string comparison in a loop? - continue; - } - propVal = propInfo.GetValue(obj, null); - } - catch (TargetInvocationException) - { - propVal = null; - } - catch (InvalidOperationException) - { - propVal = null; + if (propInfo.Name.StartsWith("NonNullable")) { + // It is a getter/setter-wrapped facade + // of a Nullable for some T - skip, + // we would inspect the raw item instead + // (factual nulls would cause an exception + // and require a try/catch overhead here). + // FIXME: Is there an attribute for this, + // to avoid a string comparison in a loop? + continue; } + var propVal = propInfo.GetValue(obj, null); if (propVal is null) { From 50e5beeb03e498fb9c78ad436733459d5d4e20e7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 02:25:26 +0200 Subject: [PATCH 241/285] BomWalkResult: SerializeBomEntity_BomRefs(): speed up check for "bom-ref" ...name/annotation by splitting into optional refinement steps Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 7e1c41cc..a1948f71 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1314,11 +1314,24 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // and consult corresponding CycloneDX spec, somehow, for // properties which have needed schema-defined type (see // detailed comments in GetBomRefsByContainer() method). - if ( - (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef") - || (Array.Find(propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true), x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null) - || (Array.Find(propInfo.GetCustomAttributes(typeof(XmlAttribute), true), x => ((XmlAttribute)x).Name == "bom-ref") != null) - ) + bool propIsBomRef = (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef"); + if (!propIsBomRef) + { + object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); + } + } + if (!propIsBomRef) + { + object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); + } + } + if (propIsBomRef) { if (!(dict.ContainsKey(container))) { From 260e95f462d26c84ca7a19bccee9df65a4a4cf33 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 02:37:09 +0200 Subject: [PATCH 242/285] BomWalkResult: SerializeBomEntity_BomRefs(): speed up objProperties[] iteration by using the cache from BomEntity Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a1948f71..fb6f5396 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1278,7 +1278,15 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) return; } - foreach (PropertyInfo propInfo in objType.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + // TODO: Prepare a similar cache with only a subset of + // properties of interest for bom-ref search, to avoid + // looking into known dead ends in a loop. + PropertyInfo[] objProperties = BomEntity.KnownEntityTypeProperties[objType]; + if (objProperties.Length < 1) + { + objProperties = objType.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + foreach (PropertyInfo propInfo in objProperties) { // We do not recurse into non-BomEntity types if (propInfo is null) From 28e9132164075062cf7c4939ec3c54837685a9a4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 12:58:03 +0200 Subject: [PATCH 243/285] BomWalkResult: rename GetBomRefsByContainer() => GetBomRefsInContainers() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index fb6f5396..0129a82c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1166,13 +1166,14 @@ public void reset(BomEntity newRoot) } /// - /// Helper for Bom.GetBomRefsByContainer(). + /// Helper for Bom.GetBomRefsInContainers(). /// /// A BomEntity instance currently being investigated /// A BomEntity instance whose attribute /// (or member of a List<> attribute) is currently being /// investigated. May be null when starting iteration - /// from this.GetBomRefsByContainer() method. + /// from this.GetBomRefsInContainers() method. + /// public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) { // With CycloneDX spec 1.4 or older it might be feasible to @@ -1321,7 +1322,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // TODO: Pedantically it would be better to either parse // and consult corresponding CycloneDX spec, somehow, for // properties which have needed schema-defined type (see - // detailed comments in GetBomRefsByContainer() method). + // detailed comments in GetBomRefsInContainers() method). bool propIsBomRef = (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef"); if (!propIsBomRef) { @@ -1444,7 +1445,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) /// See also: GetBomRefsWithContainer() with transposed returns. /// /// - public Dictionary> GetBomRefsByContainer() + public Dictionary> GetBomRefsInContainers() { return dictRefsInContainers; } @@ -1465,7 +1466,7 @@ public Dictionary> GetBomRefsByContainer() /// is attached to description of an unrelated entity. This can /// impact such operations as a FlatMerge() of different Boms. /// - /// See also: GetBomRefsByContainer() with transposed returns. + /// See also: GetBomRefsInContainers() with transposed returns. /// /// public Dictionary GetBomRefsWithContainer() From d27a1a59569c8ee6f45298d03c3c69272d88cbed Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 29 Sep 2023 14:39:57 +0200 Subject: [PATCH 244/285] Merge.cs: PoC with BomWalkResult in the loop Have some calls to gauge timing and walk in debugger Signed-off-by: Jim Klimov --- src/CycloneDX.Utils/Merge.cs | 1701 +++++++++++++++++----------------- 1 file changed, 869 insertions(+), 832 deletions(-) diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 725dc087..42e1843f 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -1,832 +1,869 @@ -// This file is part of CycloneDX Library for .NET -// -// 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. -// -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) OWASP Foundation. All Rights Reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using CycloneDX; -using CycloneDX.Models; -using CycloneDX.Models.Vulnerabilities; -using CycloneDX.Utils.Exceptions; - -namespace CycloneDX.Utils -{ - public static partial class CycloneDXUtils - { - // TOTHINK: Now that we have a BomEntity base class, shouldn't - // this logic relocate to become a Bom.MergeWith() implementation? - // Notably, sanity checks like CleanupMetadataComponent and making - // sure that a Bom+Bom merge produces a spec-validatable result - // should be a concern of that class (same as we coerce other - // classes to perform a structure-dependent meaningful merge, - // and same as the types in its source code handle non-nullable - // properties, etc.) - right?.. Perhaps sub-classes like BomFlat - // and BomHierarchical and their respective MergeWith() methods - // could be a way forward for this... - - /// - /// Performs a flat merge of two BOMs. - /// - /// Useful for situations like building a consolidated BOM for a web - /// application. Flat merge can combine the BOM for frontend code - /// with the BOM for backend code and return a single, combined BOM. - /// - /// For situations where system component hierarchy is required to be - /// maintained refer to the HierarchicalMerge method. - /// - /// - /// - /// - public static Bom FlatMerge(Bom bom1, Bom bom2) - { - return FlatMerge(bom1, bom2, BomEntityListMergeHelperStrategy.Default()); - } - - /// - /// Handle merging of two Bom object contents, possibly de-duplicating - /// or merging information from Equivalent() entries as further tuned - /// via listMergeHelperStrategy argument. - /// - /// NOTE: This sets a new timestamp into each newly merged Bom document. - /// However it is up to the caller to use Bom.BomMetadataReferThisToolkit() - /// for adding references to this library (and the run-time program - /// which consumes it) into the final merged document, to avoid the - /// overhead in a loop context. - /// - /// - /// - /// - /// - public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - { - iDebugLevel = 0; - } - - var result = new Bom(); - // Note: we recurse into this method from other FlatMerge() implementations - // (e.g. mass-merge of a big list of Bom documents), so the resulting - // document gets a new timestamp every time. It is unique after all. - // Also note that a merge of "new Bom()" with a real Bom is also different - // from that original (serialNumber, timestamp, possible entry order, etc.) - // Adding Tools[] entries to refer to this library (and the run-time tool - // program which consumes it) costs a bit more, so this is toggled separately - // and should not waste CPU not in a loop. - // Note that these toggles default to `false` so should not impact the - // typical loop (calls from the other FlatMerge() implementations nearby). - if (listMergeHelperStrategy.doBomMetadataUpdate) - { - result.BomMetadataUpdate(listMergeHelperStrategy.doBomMetadataUpdateNewSerialNumber); - } - if (listMergeHelperStrategy.doBomMetadataUpdateReferThisToolkit) - { - result.BomMetadataReferThisToolkit(); - } - if (result.Metadata is null) - { - // If none of the above... - result.Metadata = new Metadata(); - } - - #pragma warning disable 618 - var toolsMerger = new ListMergeHelper(); - #pragma warning restore 618 - var tools = toolsMerger.Merge(bom1.Metadata?.Tools?.Tools, bom2.Metadata?.Tools?.Tools, listMergeHelperStrategy); - if (tools != null) - { - if (result.Metadata.Tools == null) - { - result.Metadata.Tools = new ToolChoices(); - } - - if (result.Metadata.Tools.Tools != null) - { - tools = toolsMerger.Merge(result.Metadata.Tools.Tools, tools, listMergeHelperStrategy); - } - - result.Metadata.Tools.Tools = tools; - } - - var componentsMerger = new ListMergeHelper(); - result.Components = componentsMerger.Merge(bom1.Components, bom2.Components, listMergeHelperStrategy); - - // Add main component from bom2 as a "yet another component" - // if missing in that list so far. Note: any more complicated - // cases should be handled by CleanupMetadataComponent() when - // called by MergeCommand or similar consumer; however we can - // not generally rely in a library that only one particular - // tool calls it - so this method should ensure validity of - // its own output on every step along the way. - if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) - { - // Skip such addition if the component in bom2 is same as the - // existing metadata/component in bom1 (gluing same file together - // twice should be effectively no-op); try to merge instead: - - if (iDebugLevel >= 1) - { - Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); - } - - if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) - || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) - { - // bom1's entry is not null and seems equivalent to bom2's: - if (iDebugLevel >= 1) - { - Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); - } - result.Metadata.Component = bom1.Metadata.Component; - result.Metadata.Component.MergeWith(bom2.Metadata.Component, listMergeHelperStrategy); - } - else - { - if (iDebugLevel >= 1) - { - Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); - } - result.Components.Add(bom2.Metadata.Component); - } - } - - var servicesMerger = new ListMergeHelper(); - result.Services = servicesMerger.Merge(bom1.Services, bom2.Services, listMergeHelperStrategy); - - var extRefsMerger = new ListMergeHelper(); - result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences, listMergeHelperStrategy); - - var dependenciesMerger = new ListMergeHelper(); - result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies, listMergeHelperStrategy); - - var compositionsMerger = new ListMergeHelper(); - result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions, listMergeHelperStrategy); - - var vulnerabilitiesMerger = new ListMergeHelper(); - result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities, listMergeHelperStrategy); - - result = CleanupMetadataComponent(result); - result = CleanupEmptyLists(result); - - return result; - } - - - /// - /// Performs a flat merge of multiple BOMs. - /// - /// Useful for situations like building a consolidated BOM for a web - /// application. Flat merge can combine the BOM for frontend code - /// with the BOM for backend code and return a single, combined BOM. - /// - /// For situations where system component hierarchy is required to be - /// maintained refer to the HierarchicalMerge method. - /// - /// - /// - /// - public static Bom FlatMerge(IEnumerable boms) - { - return FlatMerge(boms, null); - } - - /// - /// Performs a flat merge of multiple BOMs. - /// - /// Useful for situations like building a consolidated BOM for a web - /// application. Flat merge can combine the BOM for frontend code - /// with the BOM for backend code and return a single, combined BOM. - /// - /// For situations where system component hierarchy is required to be - /// maintained refer to the HierarchicalMerge method. - /// - /// - /// - /// - public static Bom FlatMerge(IEnumerable boms, Component bomSubject) - { - var result = new Bom(); - BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); - BomEntityListMergeHelperStrategy quickStrategy = BomEntityListMergeHelperStrategy.Default(); - quickStrategy.useBomEntityMerge = false; - - // Sanity-check: we will do evil things in Components.MergeWith() - // among others, and hash-code based quick deduplication, which - // may potentially lead to loss of info. Keep track of "bom-ref" - // values we had incoming, and what we would see in the merged - // document eventually. - // TODO: Adapt if we would later rename conflicting entries on - // the fly. These dictionaries can help actually. See details in - // https://github.com/CycloneDX/cyclonedx-dotnet-library/pull/245#issuecomment-1686079370 - Dictionary dictBomRefsInput = CountBomRefs(result); - - // Note: we were asked to "merge" and so we do, per principle of - // least surprise - even if there is just one entry in boms[] so - // we might be inclined to skip the loop. Resulting document WILL - // differ from such single original (serialNumber, timestamp...) - int countBoms = 0; - foreach (var bom in boms) - { - // INJECTED-ERROR-FOR-TESTING // if countBoms > 1 then ... - CountBomRefs(bom, ref dictBomRefsInput); - result = FlatMerge(result, bom, quickStrategy); - countBoms++; - } - - // The quickly-made merged Bom is likely messy (only deduplicating - // identical entries). Run another merge, careful this time, over - // the resulting collection with a lot fewer items to inspect with - // the heavier logic. - var resultSubj = new Bom(); - // New merged document has its own identity (new SerialNumber, - // Version=1, Timestamp...) and its Tools collection refers to this - // library and the tool like cyclonedx-cli which consumes it. - resultSubj.BomMetadataUpdate(true); - resultSubj.BomMetadataReferThisToolkit(); - - if (bomSubject is null) - { - result = FlatMerge(resultSubj, result, safeStrategy); - } - else - { - // use the params provided if possible: prepare a new document - // with desired "metadata/component" and merge differing data - // from earlier collected result into this structure. - resultSubj.Metadata.Component = bomSubject; - resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); - CountBomRefs(resultSubj, ref dictBomRefsInput); - result = FlatMerge(resultSubj, result, safeStrategy); - - var mainDependency = new Dependency(); - mainDependency.Ref = result.Metadata.Component.BomRef; - mainDependency.Dependencies = new List(); - - // Revisit original Boms which had a metadata/component - // to write them up as dependencies of newly injected - // top-level product name. - foreach (var bom in boms) - { - if (!(bom.Metadata?.Component is null)) - { - var dep = new Dependency(); - dep.Ref = bom.Metadata.Component.BomRef; - - mainDependency.Dependencies.Add(dep); - } - } - - result.Dependencies.Add(mainDependency); - } - - result = CleanupMetadataComponent(result); - result = CleanupEmptyLists(result); - result = CleanupSortLists(result); - - // Final sanity-check: - Dictionary dictBomRefsResult = CountBomRefs(result); - if (!Enumerable.SequenceEqual(dictBomRefsResult.Keys.OrderBy(e => e), dictBomRefsInput.Keys.OrderBy(e => e))) - { - Console.WriteLine("WARNING: Different sets of 'bom-ref' in the resulting document vs. original input files!"); - } - - return result; - } - - /// - /// Performs a hierarchical merge for multiple BOMs. - /// - /// To retain system component hierarchy, top level BOM metadata - /// component must be included in each BOM. - /// - /// - /// - /// The component described by the hierarchical merge being performed. - /// - /// This will be included as the top level BOM metadata component in - /// the returned BOM. - /// - /// - public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) - { - var result = new Bom(); - // New resulting Bom has its own identity (timestamp, serial) - // and its Tools collection refers to this library and the - // tool which consumes it. - result.BomMetadataUpdate(true); - result.BomMetadataReferThisToolkit(); - - if (bomSubject != null) - { - if (bomSubject.BomRef is null) - { - bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); - } - result.Metadata.Component = bomSubject; - } - - result.Components = new List(); - result.Services = new List(); - result.ExternalReferences = new List(); - result.Dependencies = new List(); - result.Compositions = new List(); - result.Vulnerabilities = new List(); - - var bomSubjectDependencies = new List(); - - foreach (var bom in boms) - { - if (bom.Metadata?.Component is null) - { - throw new MissingMetadataComponentException( - bom.SerialNumber is null - ? "Required metadata (top level) component is missing from BOM." - : $"Required metadata (top level) component is missing from BOM {bom.SerialNumber}."); - } - - if (bom.Metadata?.Tools?.Tools?.Count > 0) - { - result.Metadata.Tools.Tools.AddRange(bom.Metadata.Tools.Tools); - } - - var thisComponent = bom.Metadata.Component; - if (thisComponent.Components is null) bom.Metadata.Component.Components = new List(); - if (!(bom.Components is null)) - { - thisComponent.Components.AddRange(bom.Components); - } - - // add a namespace to existing BOM refs - NamespaceComponentBomRefs(thisComponent); - - // make sure we have a BOM ref set and add top level dependency reference - if (thisComponent.BomRef is null) thisComponent.BomRef = ComponentBomRefNamespace(thisComponent); - bomSubjectDependencies.Add(new Dependency { Ref = thisComponent.BomRef }); - - result.Components.Add(thisComponent); - - // services - if (bom.Services != null) - foreach (var service in bom.Services) - { - service.BomRef = NamespacedBomRef(bom.Metadata.Component, service.BomRef); - result.Services.Add(service); - } - - // external references - if (!(bom.ExternalReferences is null)) result.ExternalReferences.AddRange(bom.ExternalReferences); - - // dependencies - if (bom.Dependencies != null) - { - NamespaceDependencyBomRefs(ComponentBomRefNamespace(thisComponent), bom.Dependencies); - result.Dependencies.AddRange(bom.Dependencies); - } - - // compositions - if (bom.Compositions != null) - { - NamespaceCompositions(ComponentBomRefNamespace(bom.Metadata.Component), bom.Compositions); - result.Compositions.AddRange(bom.Compositions); - } - - // vulnerabilities - if (bom.Vulnerabilities != null) - { - NamespaceVulnerabilitiesRefs(ComponentBomRefNamespace(result.Metadata.Component), bom.Vulnerabilities); - result.Vulnerabilities.AddRange(bom.Vulnerabilities); - } - } - - if (bomSubject != null) - { - result.Dependencies.Add( new Dependency - { - Ref = result.Metadata.Component.BomRef, - Dependencies = bomSubjectDependencies - }); - } - - result = CleanupMetadataComponent(result); - result = CleanupEmptyLists(result); - - return result; - } - - /// - /// Merge main "metadata/component" entry with its possible alter-ego - /// in the components list and evict extra copy from that list: per - /// spec v1_4 at least, the bom-ref must be unique across the document. - /// - /// A Bom document - /// Resulting document (whether modified or not) - public static Bom CleanupMetadataComponent(Bom result) - { - if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) - { - iDebugLevel = 0; - } - - if (iDebugLevel >= 1) - { - Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); - } - - if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) - { - BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); - if (iDebugLevel >= 2) - { - Console.WriteLine($"MERGE-CLEANUP: Searching in list"); - } - foreach (Component component in result.Components) - { - if (iDebugLevel >= 2) - { - Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); - } - if (component is null) - { - // should not happen, but... - continue; - } - if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) - { - if (iDebugLevel >= 1) - { - Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); - } - result.Metadata.Component.MergeWith(component, safeStrategy); - result.Components.Remove(component); - return result; - } - } - } - - if (iDebugLevel >= 1) - { - Console.WriteLine($"MERGE-CLEANUP: NO HITS"); - } - return result; - } - - /// - /// Clean up empty top level elements. - /// - /// A Bom document - /// Resulting document (whether modified or not) - public static Bom CleanupEmptyLists(Bom result) - { - if (result.Metadata?.Tools?.Tools?.Count == 0) - { - result.Metadata.Tools.Tools = null; - } - - if (result.Components?.Count == 0) - { - result.Components = null; - } - - if (result.Services?.Count == 0) - { - result.Services = null; - } - - if (result.ExternalReferences?.Count == 0) - { - result.ExternalReferences = null; - } - - if (result.Dependencies?.Count == 0) - { - result.Dependencies = null; - } - - if (result.Compositions?.Count == 0) - { - result.Compositions = null; - } - - if (result.Vulnerabilities?.Count == 0) - { - result.Vulnerabilities = null; - } - - return result; - } - - /// - /// Sort (top-level) list entries in the Bom for easier comparisons - /// and better compression.
- /// TODO? Drill into the BomEntities to sort lists inside too? - ///
- /// A Bom document - /// Resulting document (whether modified or not) - public static Bom CleanupSortLists(Bom result) - { - // Why oh why?.. error CS1503: Argument 1: cannot convert - // from 'System.Collections.Generic.List' - // to 'System.Collections.Generic.List' - // BomEntity.NormalizeList(result.Tools.Tools) -- it looks so simple! - // But at least we *can* call it, perhaps inefficiently for - // the run-time code and scaffolding, but easy to maintain - // with filter definitions now stored in classes, not here... - if (result.Metadata?.Tools?.Tools?.Count > 0) - { - #pragma warning disable 618 - var sortHelper = new ListMergeHelper(); - #pragma warning restore 618 - sortHelper.SortByAscending(result.Metadata.Tools.Tools, true); - } - - if (result.Components?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Components, true); - } - - if (result.Services?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Services, true); - } - - if (result.ExternalReferences?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.ExternalReferences, true); - } - - if (result.Dependencies?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Dependencies, true); - } - - if (result.Compositions?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Compositions, true); - } - - if (result.Vulnerabilities?.Count > 0) - { - var sortHelper = new ListMergeHelper(); - sortHelper.SortByAscending(result.Vulnerabilities, true); - } - - return result; - } - - // Currently our MergeWith() logic has potential to mess with - // Component bom entities (later maybe more types), and generally - // the document-wide uniqueness of BomRefs is a sore point, so - // we want them all accounted "before and after" the (flat) merge. - // Code below reuses the same dictionary object as initialized - // once for the Bom document's caller, to go faster about it: - private static void BumpDictCounter(T key, ref Dictionary dict) { - if (dict.ContainsKey(key)) { - dict[key]++; - return; - } - dict[key] = 1; - } - - private static void CountBomRefs(Component obj, ref Dictionary dict) { - if (obj is null) - { - return; - } - - if (obj.BomRef != null) - { - BumpDictCounter(obj.BomRef, ref dict); - } - - if (obj.Components != null && obj.Components.Count > 0) - { - foreach (Component child in obj.Components) - { - CountBomRefs(child, ref dict); - } - } - - if (obj.Pedigree != null) - { - if (obj.Pedigree.Ancestors != null && obj.Pedigree.Ancestors.Count > 0) - { - foreach (Component child in obj.Pedigree.Ancestors) - { - CountBomRefs(child, ref dict); - } - } - - if (obj.Pedigree.Descendants != null && obj.Pedigree.Descendants.Count > 0) - { - foreach (Component child in obj.Pedigree.Descendants) - { - CountBomRefs(child, ref dict); - } - } - - if (obj.Pedigree.Variants != null && obj.Pedigree.Variants.Count > 0) - { - foreach (Component child in obj.Pedigree.Variants) - { - CountBomRefs(child, ref dict); - } - } - } - } - - private static void CountBomRefs(Service obj, ref Dictionary dict) { - if (obj is null) - { - return; - } - - if (obj.BomRef != null) - { - BumpDictCounter(obj.BomRef, ref dict); - } - - if (obj.Services != null && obj.Services.Count > 0) - { - foreach (Service child in obj.Services) - { - CountBomRefs(child, ref dict); - } - } - } - - private static void CountBomRefs(Vulnerability obj, ref Dictionary dict) { - if (obj is null) - { - return; - } - - if (obj.BomRef != null) - { - BumpDictCounter(obj.BomRef, ref dict); - } - - // Note: Vulnerability objects are not nested (as of CDX 1.4) - } - - private static void CountBomRefs(Bom bom, ref Dictionary dict) { - if (bom is null) - { - return; - } - - if (bom.Metadata?.Component != null) { - CountBomRefs(bom.Metadata.Component, ref dict); - } - - if (bom.Components != null && bom.Components.Count > 0) - { - foreach (Component child in bom.Components) - { - CountBomRefs(child, ref dict); - } - } - - if (bom.Services != null && bom.Services.Count > 0) - { - foreach (Service child in bom.Services) - { - CountBomRefs(child, ref dict); - } - } - - if (bom.Vulnerabilities != null && bom.Vulnerabilities.Count > 0) - { - foreach (Vulnerability child in bom.Vulnerabilities) - { - CountBomRefs(child, ref dict); - } - } - } - - private static Dictionary CountBomRefs(Bom bom) { - var dict = new Dictionary(); - CountBomRefs(bom, ref dict); - return dict; - } - - private static string NamespacedBomRef(Component bomSubject, string bomRef) - { - return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); - } - - private static string NamespacedBomRef(string bomRefNamespace, string bomRef) - { - return string.IsNullOrEmpty(bomRef) ? null : $"{bomRefNamespace}:{bomRef}"; - } - - private static string ComponentBomRefNamespace(Component component) - { - return component.Group is null - ? $"{component.Name}@{component.Version}" - : $"{component.Group}.{component.Name}@{component.Version}"; - } - - private static void NamespaceComponentBomRefs(Component topComponent) - { - var components = new Stack(); - components.Push(topComponent); - - while (components.Count > 0) - { - var currentComponent = components.Pop(); - - if (currentComponent.Components != null) - { - foreach (var subComponent in currentComponent.Components) - { - components.Push(subComponent); - } - } - - currentComponent.BomRef = NamespacedBomRef(topComponent, currentComponent.BomRef); - } - } - - private static void NamespaceVulnerabilitiesRefs(string bomRefNamespace, List vulnerabilities) - { - var pendingVulnerabilities = new Stack(vulnerabilities); - - while (pendingVulnerabilities.Count > 0) - { - var vulnerability = pendingVulnerabilities.Pop(); - - vulnerability.BomRef = NamespacedBomRef(bomRefNamespace, vulnerability.BomRef); - - if (vulnerability.Affects != null) - { - foreach (var affect in vulnerability.Affects) - { - affect.Ref = bomRefNamespace; - } - } - } - } - - private static void NamespaceDependencyBomRefs(string bomRefNamespace, List dependencies) - { - var pendingDependencies = new Stack(dependencies); - - while (pendingDependencies.Count > 0) - { - var dependency = pendingDependencies.Pop(); - - if (dependency.Dependencies != null) - { - foreach (var subDependency in dependency.Dependencies) - { - pendingDependencies.Push(subDependency); - } - } - - dependency.Ref = NamespacedBomRef(bomRefNamespace, dependency.Ref); - } - } - - private static void NamespaceCompositions(string bomRefNamespace, List compositions) - { - foreach (var composition in compositions) - { - if (composition.Assemblies != null) - { - for (var i=0; i + /// Performs a flat merge of two BOMs. + /// + /// Useful for situations like building a consolidated BOM for a web + /// application. Flat merge can combine the BOM for frontend code + /// with the BOM for backend code and return a single, combined BOM. + /// + /// For situations where system component hierarchy is required to be + /// maintained refer to the HierarchicalMerge method. + ///
+ /// + /// + /// + public static Bom FlatMerge(Bom bom1, Bom bom2) + { + return FlatMerge(bom1, bom2, BomEntityListMergeHelperStrategy.Default()); + } + + /// + /// Handle merging of two Bom object contents, possibly de-duplicating + /// or merging information from Equivalent() entries as further tuned + /// via listMergeHelperStrategy argument. + /// + /// NOTE: This sets a new timestamp into each newly merged Bom document. + /// However it is up to the caller to use Bom.BomMetadataReferThisToolkit() + /// for adding references to this library (and the run-time program + /// which consumes it) into the final merged document, to avoid the + /// overhead in a loop context. + /// + /// + /// + /// + /// + public static Bom FlatMerge(Bom bom1, Bom bom2, BomEntityListMergeHelperStrategy listMergeHelperStrategy) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { + iDebugLevel = 0; + } + + /* Initial use-case for BomWalkResult discoveries to see how they scale */ + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom1..."); + } + BomWalkResult bwr1 = bom1.WalkThis(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom1: got {bwr1}"); + } + Dictionary> dict1ByC = bwr1.GetBomRefsInContainers(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom1: got {dict1ByC.Count} BomRef-entity containers"); + } + Dictionary dict1 = bwr1.GetBomRefsWithContainer(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom1: got {dict1.Count} BomRefs"); + } + + BomWalkResult bwr2 = bom2.WalkThis(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom2: got {bwr2}"); + } + Dictionary> dict2ByC = bwr2.GetBomRefsInContainers(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom2: got {dict2ByC.Count} BomRef-entity containers"); + } + Dictionary dict2 = bwr2.GetBomRefsWithContainer(); + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: {DateTime.Now}: inspecting bom2: got {dict2.Count} BomRefs"); + } + + var result = new Bom(); + // Note: we recurse into this method from other FlatMerge() implementations + // (e.g. mass-merge of a big list of Bom documents), so the resulting + // document gets a new timestamp every time. It is unique after all. + // Also note that a merge of "new Bom()" with a real Bom is also different + // from that original (serialNumber, timestamp, possible entry order, etc.) + // Adding Tools[] entries to refer to this library (and the run-time tool + // program which consumes it) costs a bit more, so this is toggled separately + // and should not waste CPU not in a loop. + // Note that these toggles default to `false` so should not impact the + // typical loop (calls from the other FlatMerge() implementations nearby). + if (listMergeHelperStrategy.doBomMetadataUpdate) + { + result.BomMetadataUpdate(listMergeHelperStrategy.doBomMetadataUpdateNewSerialNumber); + } + if (listMergeHelperStrategy.doBomMetadataUpdateReferThisToolkit) + { + result.BomMetadataReferThisToolkit(); + } + if (result.Metadata is null) + { + // If none of the above... + result.Metadata = new Metadata(); + } + + #pragma warning disable 618 + var toolsMerger = new ListMergeHelper(); + #pragma warning restore 618 + var tools = toolsMerger.Merge(bom1.Metadata?.Tools?.Tools, bom2.Metadata?.Tools?.Tools, listMergeHelperStrategy); + if (tools != null) + { + if (result.Metadata.Tools == null) + { + result.Metadata.Tools = new ToolChoices(); + } + + if (result.Metadata.Tools.Tools != null) + { + tools = toolsMerger.Merge(result.Metadata.Tools.Tools, tools, listMergeHelperStrategy); + } + + result.Metadata.Tools.Tools = tools; + } + + var componentsMerger = new ListMergeHelper(); + result.Components = componentsMerger.Merge(bom1.Components, bom2.Components, listMergeHelperStrategy); + + // Add main component from bom2 as a "yet another component" + // if missing in that list so far. Note: any more complicated + // cases should be handled by CleanupMetadataComponent() when + // called by MergeCommand or similar consumer; however we can + // not generally rely in a library that only one particular + // tool calls it - so this method should ensure validity of + // its own output on every step along the way. + if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) + { + // Skip such addition if the component in bom2 is same as the + // existing metadata/component in bom1 (gluing same file together + // twice should be effectively no-op); try to merge instead: + + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: bom1comp='{bom1.Metadata?.Component}' bom-ref1='{bom1.Metadata?.Component?.BomRef}' bom2comp='{bom2.Metadata?.Component}' bom-ref2='{bom2.Metadata?.Component?.BomRef}'"); + } + + if (!(bom1.Metadata?.Component is null) && (bom2.Metadata.Component.Equals(bom1.Metadata.Component) + || (!(bom1.Metadata?.Component?.BomRef is null) && !(bom2.Metadata?.Component?.BomRef is null) && (bom1.Metadata.Component.BomRef == bom2.Metadata.Component.BomRef)))) + { + // bom1's entry is not null and seems equivalent to bom2's: + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is already equivalent to bom2.Metadata.Component: merging"); + } + result.Metadata.Component = bom1.Metadata.Component; + result.Metadata.Component.MergeWith(bom2.Metadata.Component, listMergeHelperStrategy); + } + else + { + if (iDebugLevel >= 1) + { + Console.WriteLine($"FLAT-MERGE: bom1.Metadata.Component is missing or not equivalent to bom2.Metadata.Component: adding new entry into components[]"); + } + result.Components.Add(bom2.Metadata.Component); + } + } + + var servicesMerger = new ListMergeHelper(); + result.Services = servicesMerger.Merge(bom1.Services, bom2.Services, listMergeHelperStrategy); + + var extRefsMerger = new ListMergeHelper(); + result.ExternalReferences = extRefsMerger.Merge(bom1.ExternalReferences, bom2.ExternalReferences, listMergeHelperStrategy); + + var dependenciesMerger = new ListMergeHelper(); + result.Dependencies = dependenciesMerger.Merge(bom1.Dependencies, bom2.Dependencies, listMergeHelperStrategy); + + var compositionsMerger = new ListMergeHelper(); + result.Compositions = compositionsMerger.Merge(bom1.Compositions, bom2.Compositions, listMergeHelperStrategy); + + var vulnerabilitiesMerger = new ListMergeHelper(); + result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities, listMergeHelperStrategy); + + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + + return result; + } + + + /// + /// Performs a flat merge of multiple BOMs. + /// + /// Useful for situations like building a consolidated BOM for a web + /// application. Flat merge can combine the BOM for frontend code + /// with the BOM for backend code and return a single, combined BOM. + /// + /// For situations where system component hierarchy is required to be + /// maintained refer to the HierarchicalMerge method. + /// + /// + /// + /// + public static Bom FlatMerge(IEnumerable boms) + { + return FlatMerge(boms, null); + } + + /// + /// Performs a flat merge of multiple BOMs. + /// + /// Useful for situations like building a consolidated BOM for a web + /// application. Flat merge can combine the BOM for frontend code + /// with the BOM for backend code and return a single, combined BOM. + /// + /// For situations where system component hierarchy is required to be + /// maintained refer to the HierarchicalMerge method. + /// + /// + /// + /// + public static Bom FlatMerge(IEnumerable boms, Component bomSubject) + { + var result = new Bom(); + BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); + BomEntityListMergeHelperStrategy quickStrategy = BomEntityListMergeHelperStrategy.Default(); + quickStrategy.useBomEntityMerge = false; + + // Sanity-check: we will do evil things in Components.MergeWith() + // among others, and hash-code based quick deduplication, which + // may potentially lead to loss of info. Keep track of "bom-ref" + // values we had incoming, and what we would see in the merged + // document eventually. + // TODO: Adapt if we would later rename conflicting entries on + // the fly. These dictionaries can help actually. See details in + // https://github.com/CycloneDX/cyclonedx-dotnet-library/pull/245#issuecomment-1686079370 + Dictionary dictBomRefsInput = CountBomRefs(result); + + // Note: we were asked to "merge" and so we do, per principle of + // least surprise - even if there is just one entry in boms[] so + // we might be inclined to skip the loop. Resulting document WILL + // differ from such single original (serialNumber, timestamp...) + int countBoms = 0; + foreach (var bom in boms) + { + // INJECTED-ERROR-FOR-TESTING // if countBoms > 1 then ... + CountBomRefs(bom, ref dictBomRefsInput); + result = FlatMerge(result, bom, quickStrategy); + countBoms++; + } + + // The quickly-made merged Bom is likely messy (only deduplicating + // identical entries). Run another merge, careful this time, over + // the resulting collection with a lot fewer items to inspect with + // the heavier logic. + var resultSubj = new Bom(); + // New merged document has its own identity (new SerialNumber, + // Version=1, Timestamp...) and its Tools collection refers to this + // library and the tool like cyclonedx-cli which consumes it. + resultSubj.BomMetadataUpdate(true); + resultSubj.BomMetadataReferThisToolkit(); + + if (bomSubject is null) + { + result = FlatMerge(resultSubj, result, safeStrategy); + } + else + { + // use the params provided if possible: prepare a new document + // with desired "metadata/component" and merge differing data + // from earlier collected result into this structure. + resultSubj.Metadata.Component = bomSubject; + resultSubj.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component); + CountBomRefs(resultSubj, ref dictBomRefsInput); + result = FlatMerge(resultSubj, result, safeStrategy); + + var mainDependency = new Dependency(); + mainDependency.Ref = result.Metadata.Component.BomRef; + mainDependency.Dependencies = new List(); + + // Revisit original Boms which had a metadata/component + // to write them up as dependencies of newly injected + // top-level product name. + foreach (var bom in boms) + { + if (!(bom.Metadata?.Component is null)) + { + var dep = new Dependency(); + dep.Ref = bom.Metadata.Component.BomRef; + + mainDependency.Dependencies.Add(dep); + } + } + + result.Dependencies.Add(mainDependency); + } + + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + result = CleanupSortLists(result); + + // Final sanity-check: + Dictionary dictBomRefsResult = CountBomRefs(result); + if (!Enumerable.SequenceEqual(dictBomRefsResult.Keys.OrderBy(e => e), dictBomRefsInput.Keys.OrderBy(e => e))) + { + Console.WriteLine("WARNING: Different sets of 'bom-ref' in the resulting document vs. original input files!"); + } + + return result; + } + + /// + /// Performs a hierarchical merge for multiple BOMs. + /// + /// To retain system component hierarchy, top level BOM metadata + /// component must be included in each BOM. + /// + /// + /// + /// The component described by the hierarchical merge being performed. + /// + /// This will be included as the top level BOM metadata component in + /// the returned BOM. + /// + /// + public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) + { + var result = new Bom(); + // New resulting Bom has its own identity (timestamp, serial) + // and its Tools collection refers to this library and the + // tool which consumes it. + result.BomMetadataUpdate(true); + result.BomMetadataReferThisToolkit(); + + if (bomSubject != null) + { + if (bomSubject.BomRef is null) + { + bomSubject.BomRef = ComponentBomRefNamespace(bomSubject); + } + result.Metadata.Component = bomSubject; + } + + result.Components = new List(); + result.Services = new List(); + result.ExternalReferences = new List(); + result.Dependencies = new List(); + result.Compositions = new List(); + result.Vulnerabilities = new List(); + + var bomSubjectDependencies = new List(); + + foreach (var bom in boms) + { + if (bom.Metadata?.Component is null) + { + throw new MissingMetadataComponentException( + bom.SerialNumber is null + ? "Required metadata (top level) component is missing from BOM." + : $"Required metadata (top level) component is missing from BOM {bom.SerialNumber}."); + } + + if (bom.Metadata?.Tools?.Tools?.Count > 0) + { + result.Metadata.Tools.Tools.AddRange(bom.Metadata.Tools.Tools); + } + + var thisComponent = bom.Metadata.Component; + if (thisComponent.Components is null) bom.Metadata.Component.Components = new List(); + if (!(bom.Components is null)) + { + thisComponent.Components.AddRange(bom.Components); + } + + // add a namespace to existing BOM refs + NamespaceComponentBomRefs(thisComponent); + + // make sure we have a BOM ref set and add top level dependency reference + if (thisComponent.BomRef is null) thisComponent.BomRef = ComponentBomRefNamespace(thisComponent); + bomSubjectDependencies.Add(new Dependency { Ref = thisComponent.BomRef }); + + result.Components.Add(thisComponent); + + // services + if (bom.Services != null) + foreach (var service in bom.Services) + { + service.BomRef = NamespacedBomRef(bom.Metadata.Component, service.BomRef); + result.Services.Add(service); + } + + // external references + if (!(bom.ExternalReferences is null)) result.ExternalReferences.AddRange(bom.ExternalReferences); + + // dependencies + if (bom.Dependencies != null) + { + NamespaceDependencyBomRefs(ComponentBomRefNamespace(thisComponent), bom.Dependencies); + result.Dependencies.AddRange(bom.Dependencies); + } + + // compositions + if (bom.Compositions != null) + { + NamespaceCompositions(ComponentBomRefNamespace(bom.Metadata.Component), bom.Compositions); + result.Compositions.AddRange(bom.Compositions); + } + + // vulnerabilities + if (bom.Vulnerabilities != null) + { + NamespaceVulnerabilitiesRefs(ComponentBomRefNamespace(result.Metadata.Component), bom.Vulnerabilities); + result.Vulnerabilities.AddRange(bom.Vulnerabilities); + } + } + + if (bomSubject != null) + { + result.Dependencies.Add( new Dependency + { + Ref = result.Metadata.Component.BomRef, + Dependencies = bomSubjectDependencies + }); + } + + result = CleanupMetadataComponent(result); + result = CleanupEmptyLists(result); + + return result; + } + + /// + /// Merge main "metadata/component" entry with its possible alter-ego + /// in the components list and evict extra copy from that list: per + /// spec v1_4 at least, the bom-ref must be unique across the document. + /// + /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupMetadataComponent(Bom result) + { + if (!int.TryParse(System.Environment.GetEnvironmentVariable("CYCLONEDX_DEBUG_MERGE"), out int iDebugLevel) || iDebugLevel < 0) + { + iDebugLevel = 0; + } + + if (iDebugLevel >= 1) + { + Console.WriteLine($"MERGE-CLEANUP: metadata/component/bom-ref='{result.Metadata?.Component?.BomRef}'"); + } + + if (!(result.Metadata.Component is null) && !(result.Components is null) && (result.Components?.Count > 0) && result.Components.Contains(result.Metadata.Component)) + { + BomEntityListMergeHelperStrategy safeStrategy = BomEntityListMergeHelperStrategy.Default(); + if (iDebugLevel >= 2) + { + Console.WriteLine($"MERGE-CLEANUP: Searching in list"); + } + foreach (Component component in result.Components) + { + if (iDebugLevel >= 2) + { + Console.WriteLine($"MERGE-CLEANUP: Looking at a bom-ref='{component?.BomRef}'"); + } + if (component is null) + { + // should not happen, but... + continue; + } + if (component.Equals(result.Components) || component.BomRef.Equals(result.Metadata.Component.BomRef)) + { + if (iDebugLevel >= 1) + { + Console.WriteLine($"MERGE-CLEANUP: Found in list: merging, cleaning..."); + } + result.Metadata.Component.MergeWith(component, safeStrategy); + result.Components.Remove(component); + return result; + } + } + } + + if (iDebugLevel >= 1) + { + Console.WriteLine($"MERGE-CLEANUP: NO HITS"); + } + return result; + } + + /// + /// Clean up empty top level elements. + /// + /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupEmptyLists(Bom result) + { + if (result.Metadata?.Tools?.Tools?.Count == 0) + { + result.Metadata.Tools.Tools = null; + } + + if (result.Components?.Count == 0) + { + result.Components = null; + } + + if (result.Services?.Count == 0) + { + result.Services = null; + } + + if (result.ExternalReferences?.Count == 0) + { + result.ExternalReferences = null; + } + + if (result.Dependencies?.Count == 0) + { + result.Dependencies = null; + } + + if (result.Compositions?.Count == 0) + { + result.Compositions = null; + } + + if (result.Vulnerabilities?.Count == 0) + { + result.Vulnerabilities = null; + } + + return result; + } + + /// + /// Sort (top-level) list entries in the Bom for easier comparisons + /// and better compression.
+ /// TODO? Drill into the BomEntities to sort lists inside too? + ///
+ /// A Bom document + /// Resulting document (whether modified or not) + public static Bom CleanupSortLists(Bom result) + { + // Why oh why?.. error CS1503: Argument 1: cannot convert + // from 'System.Collections.Generic.List' + // to 'System.Collections.Generic.List' + // BomEntity.NormalizeList(result.Tools.Tools) -- it looks so simple! + // But at least we *can* call it, perhaps inefficiently for + // the run-time code and scaffolding, but easy to maintain + // with filter definitions now stored in classes, not here... + if (result.Metadata?.Tools?.Tools?.Count > 0) + { + #pragma warning disable 618 + var sortHelper = new ListMergeHelper(); + #pragma warning restore 618 + sortHelper.SortByAscending(result.Metadata.Tools.Tools, true); + } + + if (result.Components?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Components, true); + } + + if (result.Services?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Services, true); + } + + if (result.ExternalReferences?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.ExternalReferences, true); + } + + if (result.Dependencies?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Dependencies, true); + } + + if (result.Compositions?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Compositions, true); + } + + if (result.Vulnerabilities?.Count > 0) + { + var sortHelper = new ListMergeHelper(); + sortHelper.SortByAscending(result.Vulnerabilities, true); + } + + return result; + } + + // Currently our MergeWith() logic has potential to mess with + // Component bom entities (later maybe more types), and generally + // the document-wide uniqueness of BomRefs is a sore point, so + // we want them all accounted "before and after" the (flat) merge. + // Code below reuses the same dictionary object as initialized + // once for the Bom document's caller, to go faster about it: + private static void BumpDictCounter(T key, ref Dictionary dict) { + if (dict.ContainsKey(key)) { + dict[key]++; + return; + } + dict[key] = 1; + } + + private static void CountBomRefs(Component obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + if (obj.Components != null && obj.Components.Count > 0) + { + foreach (Component child in obj.Components) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree != null) + { + if (obj.Pedigree.Ancestors != null && obj.Pedigree.Ancestors.Count > 0) + { + foreach (Component child in obj.Pedigree.Ancestors) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree.Descendants != null && obj.Pedigree.Descendants.Count > 0) + { + foreach (Component child in obj.Pedigree.Descendants) + { + CountBomRefs(child, ref dict); + } + } + + if (obj.Pedigree.Variants != null && obj.Pedigree.Variants.Count > 0) + { + foreach (Component child in obj.Pedigree.Variants) + { + CountBomRefs(child, ref dict); + } + } + } + } + + private static void CountBomRefs(Service obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + if (obj.Services != null && obj.Services.Count > 0) + { + foreach (Service child in obj.Services) + { + CountBomRefs(child, ref dict); + } + } + } + + private static void CountBomRefs(Vulnerability obj, ref Dictionary dict) { + if (obj is null) + { + return; + } + + if (obj.BomRef != null) + { + BumpDictCounter(obj.BomRef, ref dict); + } + + // Note: Vulnerability objects are not nested (as of CDX 1.4) + } + + private static void CountBomRefs(Bom bom, ref Dictionary dict) { + if (bom is null) + { + return; + } + + if (bom.Metadata?.Component != null) { + CountBomRefs(bom.Metadata.Component, ref dict); + } + + if (bom.Components != null && bom.Components.Count > 0) + { + foreach (Component child in bom.Components) + { + CountBomRefs(child, ref dict); + } + } + + if (bom.Services != null && bom.Services.Count > 0) + { + foreach (Service child in bom.Services) + { + CountBomRefs(child, ref dict); + } + } + + if (bom.Vulnerabilities != null && bom.Vulnerabilities.Count > 0) + { + foreach (Vulnerability child in bom.Vulnerabilities) + { + CountBomRefs(child, ref dict); + } + } + } + + private static Dictionary CountBomRefs(Bom bom) { + var dict = new Dictionary(); + CountBomRefs(bom, ref dict); + return dict; + } + + private static string NamespacedBomRef(Component bomSubject, string bomRef) + { + return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); + } + + private static string NamespacedBomRef(string bomRefNamespace, string bomRef) + { + return string.IsNullOrEmpty(bomRef) ? null : $"{bomRefNamespace}:{bomRef}"; + } + + private static string ComponentBomRefNamespace(Component component) + { + return component.Group is null + ? $"{component.Name}@{component.Version}" + : $"{component.Group}.{component.Name}@{component.Version}"; + } + + private static void NamespaceComponentBomRefs(Component topComponent) + { + var components = new Stack(); + components.Push(topComponent); + + while (components.Count > 0) + { + var currentComponent = components.Pop(); + + if (currentComponent.Components != null) + { + foreach (var subComponent in currentComponent.Components) + { + components.Push(subComponent); + } + } + + currentComponent.BomRef = NamespacedBomRef(topComponent, currentComponent.BomRef); + } + } + + private static void NamespaceVulnerabilitiesRefs(string bomRefNamespace, List vulnerabilities) + { + var pendingVulnerabilities = new Stack(vulnerabilities); + + while (pendingVulnerabilities.Count > 0) + { + var vulnerability = pendingVulnerabilities.Pop(); + + vulnerability.BomRef = NamespacedBomRef(bomRefNamespace, vulnerability.BomRef); + + if (vulnerability.Affects != null) + { + foreach (var affect in vulnerability.Affects) + { + affect.Ref = bomRefNamespace; + } + } + } + } + + private static void NamespaceDependencyBomRefs(string bomRefNamespace, List dependencies) + { + var pendingDependencies = new Stack(dependencies); + + while (pendingDependencies.Count > 0) + { + var dependency = pendingDependencies.Pop(); + + if (dependency.Dependencies != null) + { + foreach (var subDependency in dependency.Dependencies) + { + pendingDependencies.Push(subDependency); + } + } + + dependency.Ref = NamespacedBomRef(bomRefNamespace, dependency.Ref); + } + } + + private static void NamespaceCompositions(string bomRefNamespace, List compositions) + { + foreach (var composition in compositions) + { + if (composition.Assemblies != null) + { + for (var i=0; i Date: Tue, 26 Sep 2023 16:38:32 +0200 Subject: [PATCH 245/285] BomWalkResult: add poor-man's profiling Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 157 ++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 0129a82c..a8853c50 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1151,12 +1151,61 @@ public class BomWalkResult /// readonly public Dictionary> dictBackrefs = new Dictionary>(); + // Helpers for performance accounting - how hard + // was it to discover the information in this + // BomWalkResult object? + private int sbeCountMethodEnter { get; set; } + private int sbeCountMethodQuickExit { get; set; } + private int sbeCountPropInfoEnter { get; set; } + private int sbeCountPropInfoQuickExit { get; set; } + private int sbeCountPropInfoQuickExit2 { get; set; } + private int sbeCountPropInfo { get; set; } + private int sbeCountPropInfo_EvalIsBomref { get; set; } + private int sbeCountPropInfo_EvalIsNotBomref { get; set; } + private int sbeCountPropInfo_EvalXMLAttr { get; set; } + private int sbeCountPropInfo_EvalJSONAttr { get; set; } + private int sbeCountPropInfo_EvalList { get; set; } + private int sbeCountPropInfo_EvalListQuickExit { get; set; } + private int sbeCountPropInfo_EvalListWalk { get; set; } + private int sbeCountNewBomRef { get; set; } + + // This one is null, outermost loop makes a new instance, starts and stops it: + private Stopwatch stopWatchWalkTotal = null; + private Stopwatch stopWatchEvalAttr = new Stopwatch(); + private Stopwatch stopWatchNewBomref = new Stopwatch(); + private Stopwatch stopWatchNewBomrefCheck = new Stopwatch(); + private Stopwatch stopWatchNewBomrefNewList = new Stopwatch(); + private Stopwatch stopWatchNewBomrefListAdd = new Stopwatch(); + private Stopwatch stopWatchGetValue = new Stopwatch(); + public void reset() { dictRefsInContainers.Clear(); dictBackrefs.Clear(); + sbeCountMethodEnter = 0; + sbeCountMethodQuickExit = 0; + sbeCountPropInfoEnter = 0; + sbeCountPropInfoQuickExit = 0; + sbeCountPropInfoQuickExit2 = 0; + sbeCountPropInfo = 0; + sbeCountPropInfo_EvalIsBomref = 0; + sbeCountPropInfo_EvalIsNotBomref = 0; + sbeCountPropInfo_EvalXMLAttr = 0; + sbeCountPropInfo_EvalJSONAttr = 0; + sbeCountPropInfo_EvalList = 0; + sbeCountPropInfo_EvalListQuickExit = 0; + sbeCountPropInfo_EvalListWalk = 0; + sbeCountNewBomRef = 0; + bomRoot = null; + stopWatchWalkTotal = null; + stopWatchEvalAttr = new Stopwatch(); + stopWatchNewBomref = new Stopwatch(); + stopWatchNewBomrefCheck = new Stopwatch(); + stopWatchNewBomrefNewList = new Stopwatch(); + stopWatchNewBomrefListAdd = new Stopwatch(); + stopWatchGetValue = new Stopwatch(); } public void reset(BomEntity newRoot) @@ -1165,6 +1214,48 @@ public void reset(BomEntity newRoot) this.bomRoot = newRoot; } + private static string StopWatchToString(Stopwatch stopwatch) + { + string elapsed = "N/A"; + if (stopwatch != null) + { + // Get the elapsed time as a TimeSpan value. + TimeSpan ts = stopwatch.Elapsed; + elapsed = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", + ts.Hours, ts.Minutes, ts.Seconds, + ts.Milliseconds / 10); + } + return elapsed; + } + + public override string ToString() + { + return "BomWalkResult: " + + $"Timing.WalkTotal={StopWatchToString(stopWatchWalkTotal)} " + + $"sbeCountMethodEnter={sbeCountMethodEnter} " + + $"sbeCountMethodQuickExit={sbeCountMethodQuickExit} " + + $"sbeCountPropInfoEnter={sbeCountPropInfoEnter} " + + $"sbeCountPropInfoQuickExit={sbeCountPropInfoQuickExit} " + + $"Timing.GetValue={StopWatchToString(stopWatchGetValue)} " + + $"sbeCountPropInfo_EvalIsBomref={sbeCountPropInfo_EvalIsBomref} " + + $"sbeCountPropInfo_EvalIsNotBomref={sbeCountPropInfo_EvalIsNotBomref} " + + $"Timing.EvalAttr={StopWatchToString(stopWatchEvalAttr)} " + + $"sbeCountPropInfo_EvalXMLAttr={sbeCountPropInfo_EvalXMLAttr} " + + $"sbeCountPropInfo_EvalJSONAttr={sbeCountPropInfo_EvalJSONAttr} " + + $"Timing.NewBomRef={StopWatchToString(stopWatchNewBomref)} (" + + $"Timing.NewBomRefCheck={StopWatchToString(stopWatchNewBomrefCheck)} " + + $"Timing.NewBomRefNewList={StopWatchToString(stopWatchNewBomrefNewList)} " + + $"Timing.NewBomRefListAdd={StopWatchToString(stopWatchNewBomrefListAdd)}) " + + $"sbeCountNewBomRef={sbeCountNewBomRef} " + + $"sbeCountPropInfo_EvalList={sbeCountPropInfo_EvalList} " + + $"sbeCountPropInfoQuickExit2={sbeCountPropInfoQuickExit2} " + + $"sbeCountPropInfo_EvalListQuickExit={sbeCountPropInfo_EvalListQuickExit} " + + $"sbeCountPropInfo_EvalListWalk={sbeCountPropInfo_EvalListWalk} " + + $"sbeCountPropInfo={sbeCountPropInfo} " + + $"dictRefsInContainers.Count={dictRefsInContainers.Count} " + + $"dictBackrefs.Count={dictBackrefs.Count}"; + } + /// /// Helper for Bom.GetBomRefsInContainers(). /// @@ -1265,8 +1356,11 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // The CDX spec 1.5 also introduces "annotation" which can refer to // such bom-ref carriers as service, component, organizationalEntity, // organizationalContact. + sbeCountMethodEnter++; + if (obj is null) { + sbeCountMethodQuickExit++; return; } @@ -1276,9 +1370,17 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // Hopefully the compiler or runtime would not have let other obj's in... if (objType is null || (!(typeof(BomEntity).IsAssignableFrom(objType)))) { + sbeCountMethodQuickExit++; return; } + bool isTimeAccounter = (stopWatchWalkTotal is null); + if (isTimeAccounter) + { + stopWatchWalkTotal = new Stopwatch(); + stopWatchWalkTotal.Start(); + } + // TODO: Prepare a similar cache with only a subset of // properties of interest for bom-ref search, to avoid // looking into known dead ends in a loop. @@ -1289,15 +1391,18 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) } foreach (PropertyInfo propInfo in objProperties) { + sbeCountPropInfoEnter++; + // We do not recurse into non-BomEntity types if (propInfo is null) { // Is this expected? Maybe throw? + sbeCountPropInfoQuickExit++; continue; } Type propType = propInfo.PropertyType; - + stopWatchGetValue.Start(); if (propInfo.Name.StartsWith("NonNullable")) { // It is a getter/setter-wrapped facade // of a Nullable for some T - skip, @@ -1306,12 +1411,16 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // and require a try/catch overhead here). // FIXME: Is there an attribute for this, // to avoid a string comparison in a loop? + sbeCountPropInfoQuickExit++; + stopWatchGetValue.Stop(); continue; } var propVal = propInfo.GetValue(obj, null); + stopWatchGetValue.Stop(); if (propVal is null) { + sbeCountPropInfoQuickExit++; continue; } @@ -1323,37 +1432,63 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // and consult corresponding CycloneDX spec, somehow, for // properties which have needed schema-defined type (see // detailed comments in GetBomRefsInContainers() method). + sbeCountPropInfo_EvalIsBomref++; bool propIsBomRef = (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef"); if (!propIsBomRef) { + sbeCountPropInfo_EvalIsNotBomref++; + } + if (!propIsBomRef) + { + sbeCountPropInfo_EvalXMLAttr++; + stopWatchEvalAttr.Start(); object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); if (attrs.Length > 0) { propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); } + stopWatchEvalAttr.Stop(); } if (!propIsBomRef) { + sbeCountPropInfo_EvalJSONAttr++; + stopWatchEvalAttr.Start(); object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); if (attrs.Length > 0) { propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); } + stopWatchEvalAttr.Stop(); } + if (propIsBomRef) { - if (!(dict.ContainsKey(container))) + stopWatchNewBomref.Start(); + stopWatchNewBomrefCheck.Start(); + if (!(dictRefsInContainers.ContainsKey(container))) { - dict[container] = new List(); + stopWatchNewBomrefCheck.Stop(); + stopWatchNewBomrefNewList.Start(); + dictRefsInContainers[container] = new List(); + stopWatchNewBomrefNewList.Stop(); + } + else + { + stopWatchNewBomrefCheck.Stop(); } - dict[container].Add((BomEntity)obj); + sbeCountNewBomRef++; + stopWatchNewBomrefListAdd.Start(); + dictRefsInContainers[container].Add((BomEntity)obj); + stopWatchNewBomrefListAdd.Stop(); + stopWatchNewBomref.Stop(); // Done with this string property, look at next continue; } // We do not recurse into non-BomEntity types + sbeCountPropInfo_EvalList++; bool propIsListBomEntity = ( (propType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(System.Collections.IList))) && (Array.Find(propType.GetTypeInfo().GenericTypeArguments, @@ -1366,6 +1501,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) )) { // Not a BomEntity or (potentially) a List of those + sbeCountPropInfoQuickExit2++; continue; } @@ -1392,6 +1528,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) if (listMethodGetItem == null || listPropCount == null || listMethodAdd == null) { // Should not have happened, but... + sbeCountPropInfo_EvalListQuickExit++; continue; } @@ -1399,9 +1536,11 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) if (propValCount < 1) { // Empty list + sbeCountPropInfo_EvalListQuickExit++; continue; } + sbeCountPropInfo_EvalListWalk++; for (int o = 0; o < propValCount; o++) { var listVal = listMethodGetItem.Invoke(propVal, new object[] { o }); @@ -1415,14 +1554,20 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) break; } - SerializeBomEntity_BomRefs((BomEntity)listVal, obj, ref dict); + SerializeBomEntity_BomRefs((BomEntity)listVal, obj); } // End of list, or a break per above continue; } - SerializeBomEntity_BomRefs((BomEntity)propVal, obj, ref dict); + sbeCountPropInfo++; + SerializeBomEntity_BomRefs((BomEntity)propVal, obj); + } + + if (isTimeAccounter) + { + stopWatchWalkTotal.Stop(); } } From 1daa9404ebf2eafbadd24490e107108ce63ec749 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 17:04:00 +0200 Subject: [PATCH 246/285] BomWalkResult: minimize lookups into dict by complex key Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 48 +++++++++++++++++++------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a8853c50..eeef8a67 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1167,6 +1167,7 @@ public class BomWalkResult private int sbeCountPropInfo_EvalList { get; set; } private int sbeCountPropInfo_EvalListQuickExit { get; set; } private int sbeCountPropInfo_EvalListWalk { get; set; } + private int sbeCountNewBomRefCheckDict { get; set; } private int sbeCountNewBomRef { get; set; } // This one is null, outermost loop makes a new instance, starts and stops it: @@ -1174,7 +1175,8 @@ public class BomWalkResult private Stopwatch stopWatchEvalAttr = new Stopwatch(); private Stopwatch stopWatchNewBomref = new Stopwatch(); private Stopwatch stopWatchNewBomrefCheck = new Stopwatch(); - private Stopwatch stopWatchNewBomrefNewList = new Stopwatch(); + private Stopwatch stopWatchNewBomrefNewListSpawn = new Stopwatch(); + private Stopwatch stopWatchNewBomrefNewListInDict = new Stopwatch(); private Stopwatch stopWatchNewBomrefListAdd = new Stopwatch(); private Stopwatch stopWatchGetValue = new Stopwatch(); @@ -1196,6 +1198,7 @@ public void reset() sbeCountPropInfo_EvalList = 0; sbeCountPropInfo_EvalListQuickExit = 0; sbeCountPropInfo_EvalListWalk = 0; + sbeCountNewBomRefCheckDict = 0; sbeCountNewBomRef = 0; bomRoot = null; @@ -1203,7 +1206,8 @@ public void reset() stopWatchEvalAttr = new Stopwatch(); stopWatchNewBomref = new Stopwatch(); stopWatchNewBomrefCheck = new Stopwatch(); - stopWatchNewBomrefNewList = new Stopwatch(); + stopWatchNewBomrefNewListSpawn = new Stopwatch(); + stopWatchNewBomrefNewListInDict = new Stopwatch(); stopWatchNewBomrefListAdd = new Stopwatch(); stopWatchGetValue = new Stopwatch(); } @@ -1244,8 +1248,10 @@ public override string ToString() $"sbeCountPropInfo_EvalJSONAttr={sbeCountPropInfo_EvalJSONAttr} " + $"Timing.NewBomRef={StopWatchToString(stopWatchNewBomref)} (" + $"Timing.NewBomRefCheck={StopWatchToString(stopWatchNewBomrefCheck)} " + - $"Timing.NewBomRefNewList={StopWatchToString(stopWatchNewBomrefNewList)} " + + $"Timing.NewBomRefNewListSpawn={StopWatchToString(stopWatchNewBomrefNewListSpawn)} " + + $"Timing.NewBomRefNewListInDict={StopWatchToString(stopWatchNewBomrefNewListInDict)} " + $"Timing.NewBomRefListAdd={StopWatchToString(stopWatchNewBomrefListAdd)}) " + + $"sbeCountNewBomRefCheckDict={sbeCountNewBomRefCheckDict} " + $"sbeCountNewBomRef={sbeCountNewBomRef} " + $"sbeCountPropInfo_EvalList={sbeCountPropInfo_EvalList} " + $"sbeCountPropInfoQuickExit2={sbeCountPropInfoQuickExit2} " + @@ -1381,6 +1387,13 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) stopWatchWalkTotal.Start(); } + // Looking up (comparing) keys in dictRefsInContainers[] is prohibitively + // expensive (may have to do with serialization into a string to implement + // GetHashCode() method), so we minimize interactions with that codepath. + // General assumption that we only look at same container once, but the + // code should cope with more visits (possibly at a cost). + List containerList = null; + // TODO: Prepare a similar cache with only a subset of // properties of interest for bom-ref search, to avoid // looking into known dead ends in a loop. @@ -1463,23 +1476,32 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) if (propIsBomRef) { + // Save current object into tracking, and be done with this prop! stopWatchNewBomref.Start(); - stopWatchNewBomrefCheck.Start(); - if (!(dictRefsInContainers.ContainsKey(container))) - { - stopWatchNewBomrefCheck.Stop(); - stopWatchNewBomrefNewList.Start(); - dictRefsInContainers[container] = new List(); - stopWatchNewBomrefNewList.Stop(); - } - else + if (containerList is null) { + sbeCountNewBomRefCheckDict++; + stopWatchNewBomrefCheck.Start(); + if (dictRefsInContainers.TryGetValue(container, out List list)) + { + containerList = list; + } stopWatchNewBomrefCheck.Stop(); + + if (containerList is null) + { + stopWatchNewBomrefNewListSpawn.Start(); + containerList = new List(); + stopWatchNewBomrefNewListSpawn.Stop(); + stopWatchNewBomrefNewListInDict.Start(); + dictRefsInContainers[container] = containerList; + stopWatchNewBomrefNewListInDict.Stop(); + } } sbeCountNewBomRef++; stopWatchNewBomrefListAdd.Start(); - dictRefsInContainers[container].Add((BomEntity)obj); + containerList.Add((BomEntity)obj); stopWatchNewBomrefListAdd.Stop(); stopWatchNewBomref.Stop(); From dd90ba3a7375fff6c3dd3ad76704bca7d566ea81 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 20:07:29 +0200 Subject: [PATCH 247/285] BomWalkResult: SerializeBomEntity_BomRefs(): avoid dict key lookup ...via hashing (of BomEntity) which is catastrophically slow Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index eeef8a67..c26447dc 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1482,9 +1482,25 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) { sbeCountNewBomRefCheckDict++; stopWatchNewBomrefCheck.Start(); - if (dictRefsInContainers.TryGetValue(container, out List list)) + // "proper" dict key lookup probably goes via hashes + // which go via serialization for BomEntity classes, + // and so walking a Bom with a hundred Components + // takes a second with "apparent" loop like: + // if (dictRefsInContainers.TryGetValue(container, out List list)) + // but takes miniscule fractions as it should, when + // we avoid hashing like this (and also maintain + // consistent references if original objects get + // modified - so serialization and hash changes; + // this should not happen in this loop, and the + // intention is to keep tabs on references to all + // original objects so we can rename what we need): + foreach (var (cont, list) in dictRefsInContainers) { - containerList = list; + if (Object.ReferenceEquals(container, cont)) + { + containerList = list; + break; + } } stopWatchNewBomrefCheck.Stop(); From ef7f5c47274f56c2636220ded713dec04d86bbff Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 26 Sep 2023 21:06:49 +0200 Subject: [PATCH 248/285] BomWalkResult: add a toggle for debugPerformance Toggles both accounting and reporting, each has its measurable overheads Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 171 +++++++++++++++++++------ 1 file changed, 131 insertions(+), 40 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index c26447dc..49dcae52 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1151,6 +1151,12 @@ public class BomWalkResult /// readonly public Dictionary> dictBackrefs = new Dictionary>(); + // Callers can enable performance monitoring + // (and printing in ToString() method) to help + // debug the data-walk overheads. Accounting + // does have a cost (~5% for a larger 20s run). + public bool debugPerformance = false; + // Helpers for performance accounting - how hard // was it to discover the information in this // BomWalkResult object? @@ -1234,7 +1240,7 @@ private static string StopWatchToString(Stopwatch stopwatch) public override string ToString() { - return "BomWalkResult: " + + return "BomWalkResult: " + (debugPerformance ? $"Timing.WalkTotal={StopWatchToString(stopWatchWalkTotal)} " + $"sbeCountMethodEnter={sbeCountMethodEnter} " + $"sbeCountMethodQuickExit={sbeCountMethodQuickExit} " + @@ -1257,7 +1263,8 @@ public override string ToString() $"sbeCountPropInfoQuickExit2={sbeCountPropInfoQuickExit2} " + $"sbeCountPropInfo_EvalListQuickExit={sbeCountPropInfo_EvalListQuickExit} " + $"sbeCountPropInfo_EvalListWalk={sbeCountPropInfo_EvalListWalk} " + - $"sbeCountPropInfo={sbeCountPropInfo} " + + $"sbeCountPropInfo={sbeCountPropInfo} " + : "" ) + $"dictRefsInContainers.Count={dictRefsInContainers.Count} " + $"dictBackrefs.Count={dictBackrefs.Count}"; } @@ -1362,11 +1369,17 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // The CDX spec 1.5 also introduces "annotation" which can refer to // such bom-ref carriers as service, component, organizationalEntity, // organizationalContact. - sbeCountMethodEnter++; + if (debugPerformance) + { + sbeCountMethodEnter++; + } if (obj is null) { - sbeCountMethodQuickExit++; + if (debugPerformance) + { + sbeCountMethodQuickExit++; + } return; } @@ -1376,12 +1389,15 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // Hopefully the compiler or runtime would not have let other obj's in... if (objType is null || (!(typeof(BomEntity).IsAssignableFrom(objType)))) { - sbeCountMethodQuickExit++; + if (debugPerformance) + { + sbeCountMethodQuickExit++; + } return; } bool isTimeAccounter = (stopWatchWalkTotal is null); - if (isTimeAccounter) + if (isTimeAccounter && debugPerformance) { stopWatchWalkTotal = new Stopwatch(); stopWatchWalkTotal.Start(); @@ -1404,18 +1420,27 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) } foreach (PropertyInfo propInfo in objProperties) { - sbeCountPropInfoEnter++; + if (debugPerformance) + { + sbeCountPropInfoEnter++; + } // We do not recurse into non-BomEntity types if (propInfo is null) { // Is this expected? Maybe throw? - sbeCountPropInfoQuickExit++; + if (debugPerformance) + { + sbeCountPropInfoQuickExit++; + } continue; } Type propType = propInfo.PropertyType; - stopWatchGetValue.Start(); + if (debugPerformance) + { + stopWatchGetValue.Start(); + } if (propInfo.Name.StartsWith("NonNullable")) { // It is a getter/setter-wrapped facade // of a Nullable for some T - skip, @@ -1424,16 +1449,25 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // and require a try/catch overhead here). // FIXME: Is there an attribute for this, // to avoid a string comparison in a loop? - sbeCountPropInfoQuickExit++; - stopWatchGetValue.Stop(); + if (debugPerformance) + { + sbeCountPropInfoQuickExit++; + stopWatchGetValue.Stop(); + } continue; } var propVal = propInfo.GetValue(obj, null); - stopWatchGetValue.Stop(); + if (debugPerformance) + { + stopWatchGetValue.Stop(); + } if (propVal is null) { - sbeCountPropInfoQuickExit++; + if (debugPerformance) + { + sbeCountPropInfoQuickExit++; + } continue; } @@ -1445,43 +1479,64 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // and consult corresponding CycloneDX spec, somehow, for // properties which have needed schema-defined type (see // detailed comments in GetBomRefsInContainers() method). - sbeCountPropInfo_EvalIsBomref++; + if (debugPerformance) + { + sbeCountPropInfo_EvalIsBomref++; + } bool propIsBomRef = (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef"); - if (!propIsBomRef) + if (!propIsBomRef && debugPerformance) { sbeCountPropInfo_EvalIsNotBomref++; } if (!propIsBomRef) { - sbeCountPropInfo_EvalXMLAttr++; - stopWatchEvalAttr.Start(); + if (debugPerformance) + { + sbeCountPropInfo_EvalXMLAttr++; + stopWatchEvalAttr.Start(); + } object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); if (attrs.Length > 0) { propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); } - stopWatchEvalAttr.Stop(); + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } if (!propIsBomRef) { - sbeCountPropInfo_EvalJSONAttr++; - stopWatchEvalAttr.Start(); + if (debugPerformance) + { + sbeCountPropInfo_EvalJSONAttr++; + stopWatchEvalAttr.Start(); + } object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); if (attrs.Length > 0) { propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); } - stopWatchEvalAttr.Stop(); + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } if (propIsBomRef) { // Save current object into tracking, and be done with this prop! - stopWatchNewBomref.Start(); + if (debugPerformance) + { + stopWatchNewBomref.Start(); + } if (containerList is null) { - sbeCountNewBomRefCheckDict++; - stopWatchNewBomrefCheck.Start(); + if (debugPerformance) + { + sbeCountNewBomRefCheckDict++; + stopWatchNewBomrefCheck.Start(); + } // "proper" dict key lookup probably goes via hashes // which go via serialization for BomEntity classes, // and so walking a Bom with a hundred Components @@ -1502,31 +1557,52 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) break; } } - stopWatchNewBomrefCheck.Stop(); + if (debugPerformance) + { + stopWatchNewBomrefCheck.Stop(); + } if (containerList is null) { - stopWatchNewBomrefNewListSpawn.Start(); + if (debugPerformance) + { + stopWatchNewBomrefNewListSpawn.Start(); + } containerList = new List(); - stopWatchNewBomrefNewListSpawn.Stop(); - stopWatchNewBomrefNewListInDict.Start(); + if (debugPerformance) + { + stopWatchNewBomrefNewListSpawn.Stop(); + stopWatchNewBomrefNewListInDict.Start(); + } dictRefsInContainers[container] = containerList; - stopWatchNewBomrefNewListInDict.Stop(); + if (debugPerformance) + { + stopWatchNewBomrefNewListInDict.Stop(); + } } } - sbeCountNewBomRef++; - stopWatchNewBomrefListAdd.Start(); + if (debugPerformance) + { + sbeCountNewBomRef++; + stopWatchNewBomrefListAdd.Start(); + } containerList.Add((BomEntity)obj); - stopWatchNewBomrefListAdd.Stop(); - stopWatchNewBomref.Stop(); + if (debugPerformance) + { + stopWatchNewBomrefListAdd.Stop(); + stopWatchNewBomref.Stop(); + } // Done with this string property, look at next continue; } // We do not recurse into non-BomEntity types - sbeCountPropInfo_EvalList++; + if (debugPerformance) + { + sbeCountPropInfo_EvalList++; + } bool propIsListBomEntity = ( (propType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(System.Collections.IList))) && (Array.Find(propType.GetTypeInfo().GenericTypeArguments, @@ -1539,7 +1615,10 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) )) { // Not a BomEntity or (potentially) a List of those - sbeCountPropInfoQuickExit2++; + if (debugPerformance) + { + sbeCountPropInfoQuickExit2++; + } continue; } @@ -1566,7 +1645,10 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) if (listMethodGetItem == null || listPropCount == null || listMethodAdd == null) { // Should not have happened, but... - sbeCountPropInfo_EvalListQuickExit++; + if (debugPerformance) + { + sbeCountPropInfo_EvalListQuickExit++; + } continue; } @@ -1574,11 +1656,17 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) if (propValCount < 1) { // Empty list - sbeCountPropInfo_EvalListQuickExit++; + if (debugPerformance) + { + sbeCountPropInfo_EvalListQuickExit++; + } continue; } - sbeCountPropInfo_EvalListWalk++; + if (debugPerformance) + { + sbeCountPropInfo_EvalListWalk++; + } for (int o = 0; o < propValCount; o++) { var listVal = listMethodGetItem.Invoke(propVal, new object[] { o }); @@ -1599,11 +1687,14 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } - sbeCountPropInfo++; + if (debugPerformance) + { + sbeCountPropInfo++; + } SerializeBomEntity_BomRefs((BomEntity)propVal, obj); } - if (isTimeAccounter) + if (isTimeAccounter && debugPerformance) { stopWatchWalkTotal.Stop(); } From bf591fc0afac03125d9741693284b87571839c6b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 27 Sep 2023 13:59:12 +0200 Subject: [PATCH 249/285] BomWalkResult: constrain evaluation of bom-ref fields to string types Valid assumption for current CDX spec versions (1.5 and older) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 81 ++++++++++++++++---------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 49dcae52..2c79a3c5 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1167,7 +1167,8 @@ public class BomWalkResult private int sbeCountPropInfoQuickExit2 { get; set; } private int sbeCountPropInfo { get; set; } private int sbeCountPropInfo_EvalIsBomref { get; set; } - private int sbeCountPropInfo_EvalIsNotBomref { get; set; } + private int sbeCountPropInfo_EvalIsNotStringBomref { get; set; } + private int sbeCountPropInfo_EvalIsStringNotNamedBomref { get; set; } private int sbeCountPropInfo_EvalXMLAttr { get; set; } private int sbeCountPropInfo_EvalJSONAttr { get; set; } private int sbeCountPropInfo_EvalList { get; set; } @@ -1198,7 +1199,8 @@ public void reset() sbeCountPropInfoQuickExit2 = 0; sbeCountPropInfo = 0; sbeCountPropInfo_EvalIsBomref = 0; - sbeCountPropInfo_EvalIsNotBomref = 0; + sbeCountPropInfo_EvalIsNotStringBomref = 0; + sbeCountPropInfo_EvalIsStringNotNamedBomref = 0; sbeCountPropInfo_EvalXMLAttr = 0; sbeCountPropInfo_EvalJSONAttr = 0; sbeCountPropInfo_EvalList = 0; @@ -1248,7 +1250,8 @@ public override string ToString() $"sbeCountPropInfoQuickExit={sbeCountPropInfoQuickExit} " + $"Timing.GetValue={StopWatchToString(stopWatchGetValue)} " + $"sbeCountPropInfo_EvalIsBomref={sbeCountPropInfo_EvalIsBomref} " + - $"sbeCountPropInfo_EvalIsNotBomref={sbeCountPropInfo_EvalIsNotBomref} " + + $"sbeCountPropInfo_EvalIsNotStringBomref={sbeCountPropInfo_EvalIsNotStringBomref} " + + $"sbeCountPropInfo_EvalIsStringNotNamedBomref={sbeCountPropInfo_EvalIsStringNotNamedBomref} " + $"Timing.EvalAttr={StopWatchToString(stopWatchEvalAttr)} " + $"sbeCountPropInfo_EvalXMLAttr={sbeCountPropInfo_EvalXMLAttr} " + $"sbeCountPropInfo_EvalJSONAttr={sbeCountPropInfo_EvalJSONAttr} " + @@ -1474,7 +1477,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // If the type of current "obj" contains a "bom-ref", or // has annotations like [JsonPropertyName("bom-ref")] and // [XmlAttribute("bom-ref")], save it into the dictionary. - + // // TODO: Pedantically it would be better to either parse // and consult corresponding CycloneDX spec, somehow, for // properties which have needed schema-defined type (see @@ -1483,43 +1486,61 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) { sbeCountPropInfo_EvalIsBomref++; } - bool propIsBomRef = (propType.GetTypeInfo().IsAssignableFrom(typeof(string)) && propInfo.Name == "BomRef"); - if (!propIsBomRef && debugPerformance) - { - sbeCountPropInfo_EvalIsNotBomref++; - } - if (!propIsBomRef) + bool propIsBomRef = false; + if (propType.GetTypeInfo().IsAssignableFrom(typeof(string))) { - if (debugPerformance) + // NOTE: Current CycloneDX spec (1.5 and those before it) + // explicitly specify reference fields as a string type. + // Wondering if this would change in the future (more so + // with higher-level grouping types like "refLinkType" or + // "bomLink", or generic "link to somewhere" such as + // "anyOf refLinkType or bomLinkElementType") which are + // a frequent occurrence starting from CDX spec 1.5... + propIsBomRef = (propInfo.Name == "BomRef"); + if (!propIsBomRef && debugPerformance) { - sbeCountPropInfo_EvalXMLAttr++; - stopWatchEvalAttr.Start(); + sbeCountPropInfo_EvalIsStringNotNamedBomref++; } - object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); - if (attrs.Length > 0) + if (!propIsBomRef) { - propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); + if (debugPerformance) + { + sbeCountPropInfo_EvalXMLAttr++; + stopWatchEvalAttr.Start(); + } + object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); + } + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } - if (debugPerformance) + if (!propIsBomRef) { - stopWatchEvalAttr.Stop(); + if (debugPerformance) + { + sbeCountPropInfo_EvalJSONAttr++; + stopWatchEvalAttr.Start(); + } + object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); + } + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } } - if (!propIsBomRef) + else { if (debugPerformance) { - sbeCountPropInfo_EvalJSONAttr++; - stopWatchEvalAttr.Start(); - } - object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); - if (attrs.Length > 0) - { - propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); - } - if (debugPerformance) - { - stopWatchEvalAttr.Stop(); + sbeCountPropInfo_EvalIsNotStringBomref++; } } From 851c978918c48e7c521c96e002934ad05c260f87 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 28 Sep 2023 14:18:25 +0200 Subject: [PATCH 250/285] BomWalkResult: add tracking of "string Ref" back-links Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 113 +++++++++++++++++++------ 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 2c79a3c5..ba6f3543 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1169,6 +1169,7 @@ public class BomWalkResult private int sbeCountPropInfo_EvalIsBomref { get; set; } private int sbeCountPropInfo_EvalIsNotStringBomref { get; set; } private int sbeCountPropInfo_EvalIsStringNotNamedBomref { get; set; } + private int sbeCountPropInfo_EvalIsStringNotNamedRef { get; set; } private int sbeCountPropInfo_EvalXMLAttr { get; set; } private int sbeCountPropInfo_EvalJSONAttr { get; set; } private int sbeCountPropInfo_EvalList { get; set; } @@ -1185,6 +1186,7 @@ public class BomWalkResult private Stopwatch stopWatchNewBomrefNewListSpawn = new Stopwatch(); private Stopwatch stopWatchNewBomrefNewListInDict = new Stopwatch(); private Stopwatch stopWatchNewBomrefListAdd = new Stopwatch(); + private Stopwatch stopWatchNewRefLink = new Stopwatch(); private Stopwatch stopWatchGetValue = new Stopwatch(); public void reset() @@ -1201,6 +1203,7 @@ public void reset() sbeCountPropInfo_EvalIsBomref = 0; sbeCountPropInfo_EvalIsNotStringBomref = 0; sbeCountPropInfo_EvalIsStringNotNamedBomref = 0; + sbeCountPropInfo_EvalIsStringNotNamedRef = 0; sbeCountPropInfo_EvalXMLAttr = 0; sbeCountPropInfo_EvalJSONAttr = 0; sbeCountPropInfo_EvalList = 0; @@ -1217,6 +1220,7 @@ public void reset() stopWatchNewBomrefNewListSpawn = new Stopwatch(); stopWatchNewBomrefNewListInDict = new Stopwatch(); stopWatchNewBomrefListAdd = new Stopwatch(); + stopWatchNewRefLink = new Stopwatch(); stopWatchGetValue = new Stopwatch(); } @@ -1252,6 +1256,7 @@ public override string ToString() $"sbeCountPropInfo_EvalIsBomref={sbeCountPropInfo_EvalIsBomref} " + $"sbeCountPropInfo_EvalIsNotStringBomref={sbeCountPropInfo_EvalIsNotStringBomref} " + $"sbeCountPropInfo_EvalIsStringNotNamedBomref={sbeCountPropInfo_EvalIsStringNotNamedBomref} " + + $"sbeCountPropInfo_EvalIsStringNotNamedRef={sbeCountPropInfo_EvalIsStringNotNamedRef} " + $"Timing.EvalAttr={StopWatchToString(stopWatchEvalAttr)} " + $"sbeCountPropInfo_EvalXMLAttr={sbeCountPropInfo_EvalXMLAttr} " + $"sbeCountPropInfo_EvalJSONAttr={sbeCountPropInfo_EvalJSONAttr} " + @@ -1262,6 +1267,7 @@ public override string ToString() $"Timing.NewBomRefListAdd={StopWatchToString(stopWatchNewBomrefListAdd)}) " + $"sbeCountNewBomRefCheckDict={sbeCountNewBomRefCheckDict} " + $"sbeCountNewBomRef={sbeCountNewBomRef} " + + $"Timing.NewRefLink={StopWatchToString(stopWatchNewRefLink)} (" + $"sbeCountPropInfo_EvalList={sbeCountPropInfo_EvalList} " + $"sbeCountPropInfoQuickExit2={sbeCountPropInfoQuickExit2} " + $"sbeCountPropInfo_EvalListQuickExit={sbeCountPropInfo_EvalListQuickExit} " + @@ -1332,8 +1338,15 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // or "component or service" (since 1.5) // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" + // ** NOTE: As of this writing, Composition.cs file + // defines assemblies[] and dependencies[] as lists + // of strings, each treated as a "ref" in class + // instance (de-)serializations // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" // * (1.5) componentEvidence/identity/tools[] => any, see spec + // ** NOTE: As of this writing, EvidenceTools.cs is + // defined as a list of strings, each treated as + // a "ref" in class instance (de-)serializations // * (1.5) annotations/subjects[] => any // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") // * (1.5) resourceReferenceChoice/"ref" => any @@ -1487,6 +1500,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) sbeCountPropInfo_EvalIsBomref++; } bool propIsBomRef = false; + bool propIsRefLink = false; if (propType.GetTypeInfo().IsAssignableFrom(typeof(string))) { // NOTE: Current CycloneDX spec (1.5 and those before it) @@ -1497,42 +1511,53 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // "anyOf refLinkType or bomLinkElementType") which are // a frequent occurrence starting from CDX spec 1.5... propIsBomRef = (propInfo.Name == "BomRef"); - if (!propIsBomRef && debugPerformance) - { - sbeCountPropInfo_EvalIsStringNotNamedBomref++; - } if (!propIsBomRef) { if (debugPerformance) { - sbeCountPropInfo_EvalXMLAttr++; - stopWatchEvalAttr.Start(); - } - object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); - if (attrs.Length > 0) - { - propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); - } - if (debugPerformance) - { - stopWatchEvalAttr.Stop(); + sbeCountPropInfo_EvalIsStringNotNamedBomref++; } + propIsRefLink = (propInfo.Name == "Ref"); } - if (!propIsBomRef) + if (!propIsRefLink) { if (debugPerformance) { - sbeCountPropInfo_EvalJSONAttr++; - stopWatchEvalAttr.Start(); + sbeCountPropInfo_EvalIsStringNotNamedRef++; } - object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); - if (attrs.Length > 0) + if (!propIsBomRef) { - propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); + if (debugPerformance) + { + sbeCountPropInfo_EvalXMLAttr++; + stopWatchEvalAttr.Start(); + } + object[] attrs = propInfo.GetCustomAttributes(typeof(XmlAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((XmlAttribute)x).Name == "bom-ref") != null); + } + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } - if (debugPerformance) + if (!propIsBomRef) { - stopWatchEvalAttr.Stop(); + if (debugPerformance) + { + sbeCountPropInfo_EvalJSONAttr++; + stopWatchEvalAttr.Start(); + } + object[] attrs = propInfo.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false); + if (attrs.Length > 0) + { + propIsBomRef = (Array.Find(attrs, x => ((JsonPropertyNameAttribute)x).Name == "bom-ref") != null); + } + if (debugPerformance) + { + stopWatchEvalAttr.Stop(); + } } } } @@ -1615,7 +1640,47 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) stopWatchNewBomref.Stop(); } - // Done with this string property, look at next + // Done with this (string) property, look at next + continue; + } + + if (propIsRefLink) + { + // Save current object into "back-reference" tracking, + // and be done with this prop! + // Note: this approach covers only string "ref" properties, + // but not those few with a "List" at the moment! + // Note: It is currently somewhat up to the consumer + // of these results to guess (or find) which "obj" + // property is the reference (currently tends to be + // called "Ref", but...). For the greater purposes of + // entities' "bom-ref" renaming this could surely be + // optimized. + if (debugPerformance) + { + stopWatchNewRefLink.Start(); + } + + string sPropVal = (string)propVal; + // nullness ruled out above + if (sPropVal.Trim() == "") + { + continue; + } + + if (!(dictBackrefs.TryGetValue(sPropVal, out List listBackrefs))) + { + listBackrefs = new List(); + dictBackrefs[sPropVal] = listBackrefs; + } + listBackrefs.Add(obj); + + if (debugPerformance) + { + stopWatchNewRefLink.Stop(); + } + + // Done with this (string) property, look at next continue; } From e1da8c6e5e759669610689d01c7bc05a38c8b804 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 28 Sep 2023 14:27:36 +0200 Subject: [PATCH 251/285] BomEntity.cs: extend the family with IBomEntity* interfaces ...to tag certain use-case patterns in source: this is relatively compact in terms of copy-paste, and cheaper for generalized approach than "blind" reflection-based walks. Can eventually optimize BomWalkResult: SerializeBomEntity_BomRefs(), but even more so - simplify writing consumers of those walk results that would act on existing object instances in a Bom document. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 118 ++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ba6f3543..cb0980df 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -421,6 +421,120 @@ public class BomEntityListMergeHelperReflection public Object helperInstance { get; set; } } + /// + /// Just a baseline interface for the big BomEntity + /// family to formally implement. In practice all + /// those classes are derived from BomEntity so it + /// can dispatch calls into them when used as a + /// generic base class, or serve default method + /// implementations. + /// + public interface IBomEntity : IEquatable + { + public string SerializeEntity(); + } + + /// + /// Interface assigned to BomEntity derived classes + /// which have a property generally conforming to + /// CycloneDX schema definition of "bom:refType" + /// (per XML schema) or "#/definitions/refType" + /// (per JSON schema). + /// Such a property is usually called "bom-ref" + /// in text representations of Bom documents and + /// is a C# string; however some more complex type + /// may be used in the future to multi-plex all the + /// different referencing use-cases. + /// + /// For specific practical hints, see also: + /// IBomEntityWithRefType_String_BomRef + /// + public interface IBomEntityWithRefType : IBomEntity + { + } + + /// + /// Interface assigned to BomEntity derived classes + /// which have a property with a CycloneDX Bom schema + /// "refType" attribute specifically named "BomRef" + /// and typed as a "string" in C#. It helps to know + /// where we can call GetBomRef() safely... + /// + public interface IBomEntityWithRefType_String_BomRef : IBomEntityWithRefType + { + } + + /// + /// Interface assigned to BomEntity derived classes + /// which have a property generally conforming to + /// CycloneDX schema definition of + /// "bom:refLinkType" (per XML schema) or + /// "#/definitions/refLinkType" (per JSON schema). + /// Such a property is usually called "ref" + /// in text representations of Bom documents, + /// but can be items in certain lists as well. + /// + /// Technically it follows same schema definition + /// as a "refType" but is intended (since CDX 1.5) + /// to specify links pointing to someone else's + /// "bom-ref" values. + /// + /// For specific practical hints, see also: + /// IBomEntityWithRefLinkType_String_Ref + /// IBomEntityWithRefLinkType_StringList + /// + public interface IBomEntityWithRefLinkType : IBomEntity + { + /// + /// For each property in this class which can + /// convey a Bom "refLinkType" (single values + /// like a "ref" or lists full of references), + /// clarify which classes are expected to be + /// on the other end of the reference -- with + /// one of their instances having the "bom-ref" + /// identification value specified in this "ref". + /// The CycloneDX spec details that some refs + /// only point to a "component", others also + /// to a "service", some to a "componentData", + /// and some do not constrain. + /// + /// Note that there may be no hits in the + /// current Bom document, and not all items + /// with a "bom-ref" attribute would have + /// such back-links to them defined in the + /// same Bom document. + /// + /// + // FIXME: Would a C# annotation serve this cause + // better? Would it be faster in processing + // (with reflection) e.g. to *find* which + // properties to look at? + public Dictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion); + } + + /// + /// Interface assigned to BomEntity derived classes + /// which have a property with a CycloneDX Bom schema + /// "refLinkType" attribute specifically named "Ref" + /// and typed as a "string" in C#. It helps to know + /// where we can call GetRef() safely... + /// + public interface IBomEntityWithRefLinkType_String_Ref : IBomEntityWithRefLinkType + { + } + + /// + /// Interface assigned to BomEntity derived classes + /// which have one or more properties which are lists, + /// whose items conform to CycloneDX Bom schema for + /// "refLinkType", and are typed as a "List" + /// in C#. It helps to know where we can iterate + /// those safely... See also GetRefLinkConstraints(). + /// + public interface IBomEntityWithRefLinkType_StringList : IBomEntityWithRefLinkType + { + } + /// /// BomEntity is intended as a base class for other classes in CycloneDX.Models, /// which in turn encapsulate different concepts and data types described by @@ -430,7 +544,7 @@ public class BomEntityListMergeHelperReflection /// and to define the logic for merge-ability of such objects while coding much /// of the logical scaffolding only once. /// - public class BomEntity : IEquatable + public class BomEntity : IBomEntity { // Keep this info initialized once to cut down on overheads of reflection // when running in our run-time loops. @@ -777,7 +891,7 @@ public string SerializeEntity() /// /// Another BomEntity-derived object of same type /// True if two objects are deemed equal - public bool Equals(BomEntity obj) + public bool Equals(IBomEntity obj) { Type thisType = this.GetType(); if (KnownTypeEquals.TryGetValue(thisType, out var methodEquals)) From 288ef3179690ffaf9f4ca6f0023b5ac030690c68 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 28 Sep 2023 20:18:54 +0200 Subject: [PATCH 252/285] BomWalkResult: clarify comments about ref/bomref occurences in spec Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index cb0980df..78845d93 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1452,18 +1452,32 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // or "component or service" (since 1.5) // * (1.4, 1.5) compositions/"assemblies[]" => "component or service" // * (1.4, 1.5) compositions/"dependencies[]" => "component or service" + // * (1.5) compositions/"vulnerabilities[]" => "vulnerability" // ** NOTE: As of this writing, Composition.cs file - // defines assemblies[] and dependencies[] as lists - // of strings, each treated as a "ref" in class - // instance (de-)serializations + // defines Assemblies[], Dependencies[] and + // Vulnerabilities[] as lists of strings, + // each treated as a "ref" in class instance + // (de-)serializations + // ** (1.5) Of these, Assemblies[] may be either + // refLinkType or bomLinkElementType // * (1.4, 1.5) vulnerability/affects/items/"ref" => "component or service" + // ** May be either refLinkType or bomLinkElementType // * (1.5) componentEvidence/identity/tools[] => any, see spec + // ** May be either refLinkType or bomLinkElementType // ** NOTE: As of this writing, EvidenceTools.cs is // defined as a list of strings, each treated as // a "ref" in class instance (de-)serializations // * (1.5) annotations/subjects[] => any - // * (1.5) modelCard/modelParameters/datasets[]/"ref" => "data component" (see "#/definitions/componentData") + // ** May be either refLinkType or bomLinkElementType + // ** In C# stored as List and exposed as + // a dynamically built List - this one is + // not of interest to the walk + // * (1.5) modelCard/modelParameters/datasets[]/"ref" => + // "data component" (see "#/definitions/componentData") + // ** May be either refLinkType or bomLinkElementType // * (1.5) resourceReferenceChoice/"ref" => any + // ** May be either refLinkType or bomLinkElementType + // ** Used as a generalized reference type, summarized below // // Notably, CDX 1.5 also introduces resourceReferenceChoice // which generalizes internal or external references, used in: From 3b203ffe621fc88ec93e7388f085f83f6985734e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 29 Sep 2023 10:45:42 +0200 Subject: [PATCH 253/285] BomWalkResult: add tracking of "List Something" back-links Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 77 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 78845d93..b330189c 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1286,6 +1286,7 @@ public class BomWalkResult private int sbeCountPropInfo_EvalIsStringNotNamedRef { get; set; } private int sbeCountPropInfo_EvalXMLAttr { get; set; } private int sbeCountPropInfo_EvalJSONAttr { get; set; } + private int sbeCountPropInfo_EvalIsRefLinkListString { get; set; } private int sbeCountPropInfo_EvalList { get; set; } private int sbeCountPropInfo_EvalListQuickExit { get; set; } private int sbeCountPropInfo_EvalListWalk { get; set; } @@ -1301,6 +1302,7 @@ public class BomWalkResult private Stopwatch stopWatchNewBomrefNewListInDict = new Stopwatch(); private Stopwatch stopWatchNewBomrefListAdd = new Stopwatch(); private Stopwatch stopWatchNewRefLink = new Stopwatch(); + private Stopwatch stopWatchNewRefLinkListString = new Stopwatch(); private Stopwatch stopWatchGetValue = new Stopwatch(); public void reset() @@ -1320,6 +1322,7 @@ public void reset() sbeCountPropInfo_EvalIsStringNotNamedRef = 0; sbeCountPropInfo_EvalXMLAttr = 0; sbeCountPropInfo_EvalJSONAttr = 0; + sbeCountPropInfo_EvalIsRefLinkListString = 0; sbeCountPropInfo_EvalList = 0; sbeCountPropInfo_EvalListQuickExit = 0; sbeCountPropInfo_EvalListWalk = 0; @@ -1335,6 +1338,7 @@ public void reset() stopWatchNewBomrefNewListInDict = new Stopwatch(); stopWatchNewBomrefListAdd = new Stopwatch(); stopWatchNewRefLink = new Stopwatch(); + stopWatchNewRefLinkListString = new Stopwatch(); stopWatchGetValue = new Stopwatch(); } @@ -1374,6 +1378,7 @@ public override string ToString() $"Timing.EvalAttr={StopWatchToString(stopWatchEvalAttr)} " + $"sbeCountPropInfo_EvalXMLAttr={sbeCountPropInfo_EvalXMLAttr} " + $"sbeCountPropInfo_EvalJSONAttr={sbeCountPropInfo_EvalJSONAttr} " + + $"sbeCountPropInfo_EvalIsRefLinkListString={sbeCountPropInfo_EvalIsRefLinkListString} " + $"Timing.NewBomRef={StopWatchToString(stopWatchNewBomref)} (" + $"Timing.NewBomRefCheck={StopWatchToString(stopWatchNewBomrefCheck)} " + $"Timing.NewBomRefNewListSpawn={StopWatchToString(stopWatchNewBomrefNewListSpawn)} " + @@ -1381,7 +1386,8 @@ public override string ToString() $"Timing.NewBomRefListAdd={StopWatchToString(stopWatchNewBomrefListAdd)}) " + $"sbeCountNewBomRefCheckDict={sbeCountNewBomRefCheckDict} " + $"sbeCountNewBomRef={sbeCountNewBomRef} " + - $"Timing.NewRefLink={StopWatchToString(stopWatchNewRefLink)} (" + + $"Timing.NewRefLink={StopWatchToString(stopWatchNewRefLink)} " + + $"Timing.NewRefLinkListString={StopWatchToString(stopWatchNewRefLinkListString)} " + $"sbeCountPropInfo_EvalList={sbeCountPropInfo_EvalList} " + $"sbeCountPropInfoQuickExit2={sbeCountPropInfoQuickExit2} " + $"sbeCountPropInfo_EvalListQuickExit={sbeCountPropInfo_EvalListQuickExit} " + @@ -1629,6 +1635,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) } bool propIsBomRef = false; bool propIsRefLink = false; + bool propIsRefLinkListString = false; if (propType.GetTypeInfo().IsAssignableFrom(typeof(string))) { // NOTE: Current CycloneDX spec (1.5 and those before it) @@ -1695,6 +1702,29 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) { sbeCountPropInfo_EvalIsNotStringBomref++; } + + // Check for those few variables which are lists of strings + // with "ref"-like items. + // As noted above, "annotations/subjects[]" are not handled + // here as a list of strings, because that is a shim view. + if (propType.GetTypeInfo().IsAssignableFrom(typeof(List))) + { + if (debugPerformance) + { + sbeCountPropInfo_EvalIsRefLinkListString++; + } + + if (( + objType == typeof(Composition) && + (propInfo.Name == "Assemblies" + || propInfo.Name == "Dependencies" + || propInfo.Name == "Vulnerabilities") + ) || objType == typeof(EvidenceTools) + ) + { + propIsRefLinkListString = true; + } + } } if (propIsBomRef) @@ -1777,7 +1807,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // Save current object into "back-reference" tracking, // and be done with this prop! // Note: this approach covers only string "ref" properties, - // but not those few with a "List" at the moment! + // but not those few with a "List" - handled below. // Note: It is currently somewhat up to the consumer // of these results to guess (or find) which "obj" // property is the reference (currently tends to be @@ -1812,6 +1842,49 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) continue; } + if (propIsRefLinkListString) + { + // Save current object into "back-reference" tracking, + // and be done with this prop! + // Note: It is currently somewhat up to the consumer + // of these results to guess (or find) which "obj" + // property is the list with the reference (and which + // list item, by number). For the greater purposes of + // entities' "bom-ref" renaming this could surely be + // optimized. + if (debugPerformance) + { + stopWatchNewRefLinkListString.Start(); + } + + List lsPropVal = (List)propVal; + if (lsPropVal.Count > 0) + { + // Walk all items and list in backrefs pointing to this object + foreach (string sPropVal in lsPropVal) + { + if (sPropVal is null || sPropVal.Trim() == "") + { + continue; + } + if (!(dictBackrefs.TryGetValue(sPropVal, out List listBackrefs))) + { + listBackrefs = new List(); + dictBackrefs[sPropVal] = listBackrefs; + } + listBackrefs.Add(obj); + } + } + + if (debugPerformance) + { + stopWatchNewRefLinkListString.Stop(); + } + + // Done with this (string) property, look at next + continue; + } + // We do not recurse into non-BomEntity types if (debugPerformance) { From e844169161b42469a9b42ed11c54d87e759b08f0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 29 Sep 2023 15:34:06 +0200 Subject: [PATCH 254/285] Bom.cs: introduce an AssertThisBomWalkResult() helper Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 36 +++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 22061002..47652999 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -325,6 +325,29 @@ public BomWalkResult WalkThis() return res; } + /// + /// Helper for sanity-check of inputs for methods that deal + /// with BomWalkResult arguments that should refer to "this" + /// exact Bom document instance as their bomRoot. + /// + /// Result of an earlier Bom.WalkThis() or equivalent call + /// The "res" argument should be non-null + /// The "res" argument should point to this Bom instance + private void AssertThisBomWalkResult(BomWalkResult res) + { + if (res == null) + { + throw new ArgumentNullException("res"); + } + + if (!(Object.ReferenceEquals(res.bomRoot, this))) + { + throw new BomEntityConflictException( + "The specified BomWalkResult.bomRoot does not refer to this Bom document instance", + res.bomRoot.GetType()); + } + } + /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained @@ -346,11 +369,7 @@ public BomWalkResult WalkThis() /// public Dictionary> GetBomRefsInContainers(BomWalkResult res) { - if (res.bomRoot != this) - { - // throw? - return null; - } + AssertThisBomWalkResult(res); return res.dictRefsInContainers; } @@ -387,11 +406,7 @@ public Dictionary> GetBomRefsInContainers() /// public Dictionary GetBomRefsWithContainer(BomWalkResult res) { - if (res.bomRoot != this) - { - // throw? - return null; - } + AssertThisBomWalkResult(res); return res.GetBomRefsWithContainer(); } @@ -433,6 +448,7 @@ public Dictionary GetBomRefsWithContainer() /// public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) { + AssertThisBomWalkResult(res); return false; } From 686ab5d9fdae33c7c26e4fc89f448df8c5cca7bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 29 Sep 2023 15:57:02 +0200 Subject: [PATCH 255/285] BomWalkResult: do not constrain refs that they may not be pure whitespace (must be not-empty though) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index b330189c..451bdced 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1821,7 +1821,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) string sPropVal = (string)propVal; // nullness ruled out above - if (sPropVal.Trim() == "") + if (sPropVal == "") { continue; } @@ -1863,7 +1863,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // Walk all items and list in backrefs pointing to this object foreach (string sPropVal in lsPropVal) { - if (sPropVal is null || sPropVal.Trim() == "") + if (sPropVal is null || sPropVal == "") { continue; } From 574cdd3b88a9a94b80560efc407ea2b09af57c64 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 00:32:51 +0200 Subject: [PATCH 256/285] BomEntity: make sure interfaces IBomEntityWithRefType_String_BomRef and IBomEntityWithRefLinkType_String_Ref declare the getter/setter methods for easy generic use Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 451bdced..d10057b3 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -440,6 +440,7 @@ public interface IBomEntity : IEquatable /// CycloneDX schema definition of "bom:refType" /// (per XML schema) or "#/definitions/refType" /// (per JSON schema). + /// /// Such a property is usually called "bom-ref" /// in text representations of Bom documents and /// is a C# string; however some more complex type @@ -462,6 +463,8 @@ public interface IBomEntityWithRefType : IBomEntity /// public interface IBomEntityWithRefType_String_BomRef : IBomEntityWithRefType { + public string GetBomRef(); + public void SetBomRef(string s); } /// @@ -521,6 +524,8 @@ public interface IBomEntityWithRefLinkType : IBomEntity /// public interface IBomEntityWithRefLinkType_String_Ref : IBomEntityWithRefLinkType { + public string GetRef(); + public void SetRef(string s); } /// From 75d3c1711d372c01e22fb83d8fca3db08e9482c0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 00:33:37 +0200 Subject: [PATCH 257/285] BomEntity: BomWalkResult: rectify drilling into EvidenceIdentity/Tools[] vs. (probably lacking) EvidenceTools[] Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index d10057b3..85055906 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1724,7 +1724,9 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) (propInfo.Name == "Assemblies" || propInfo.Name == "Dependencies" || propInfo.Name == "Vulnerabilities") - ) || objType == typeof(EvidenceTools) + ) || (objType == typeof(EvidenceIdentity) && + propInfo.Name == "Tools" + ) || objType == typeof(EvidenceTools) // Actually this should not hit, presumably, as its "obj" is not a BomEntity and the EvidenceIdentity contains this (list class) as a property ) { propIsRefLinkListString = true; From ee524e7b27ef0ff68ec336618c4bf3bc4c7422ec Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 04:04:14 +0200 Subject: [PATCH 258/285] Bom.cs: implement the logic for RenameBomRef() method Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 492 ++++++++++++++++++++++++++++++- 1 file changed, 491 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 47652999..c0bb0229 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -449,7 +449,497 @@ public Dictionary GetBomRefsWithContainer() public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) { AssertThisBomWalkResult(res); - return false; + if (oldRef is null || newRef is null || oldRef == newRef) + { + // Non-fatal, but no-op + // Note: not checking for xxxRef.Trim()=="" or trimmed-string + // equalities as it is up to the caller how things were or + // will be named. + return false; + } + + if (newRef == "") + { + throw new ArgumentException("newRef is empty, must be at least 1 char"); + } + + // First check if there is anything to rename, and if the name is + // already known as somebody's identifier. + Dictionary dictBomrefs = res.GetBomRefsWithContainer(); + // At most we have one(!) object with "oldRef" name as its identifier: + BomEntity namedObject = null; + BomEntity namedObjectContainer = null; + foreach (var (contained, container) in dictBomrefs) + { + // Here and below: if casting fails and throws... + // it is the right thing to do in given situation :) + object containedBomRef = null; + if (contained is IBomEntityWithRefType_String_BomRef) + { + containedBomRef = ((IBomEntityWithRefType_String_BomRef)contained).GetBomRef(); + } + else + { + var propInfo = contained.GetType().GetProperty("BomRef", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + contained.GetType().Name); + } + containedBomRef = propInfo.GetValue(contained); + } + + if (containedBomRef.ToString() == oldRef) + { + if (namedObject != null) + { + throw new BomEntityConflictException("Duplicate \"bom-ref\" identifier detected in Bom document: " + oldRef); + } + namedObject = contained; + namedObjectContainer = container; + // Do not "break" the loop, so we can detect dupes and newRef clashes here + } + + if (containedBomRef.ToString() == newRef) + { + throw new ArgumentException("newRef is already used to name a BomEntity: " + newRef); + } + } + + // If we got here, the oldRef name exists among + // "contained" entities, and newRef does not. + + // Can proceed with renaming of the item itself (if one exists)...: + if (!(namedObject is null)) + { + bool objectHasStringBomRef = (namedObject is IBomEntityWithRefType_String_BomRef); + + if (!objectHasStringBomRef) + { + // Slower fallback to facilitate faster code evolution + // (with classes not marked as implementors of interfaces) + if (!(BomEntity.KnownEntityTypeProperties.TryGetValue(namedObject.GetType(), out PropertyInfo[] props))) + { + props = namedObject.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + } + + foreach (PropertyInfo propInfo in props) + { + if (propInfo.Name == "BomRef" && propInfo.PropertyType == typeof(string)) + { + objectHasStringBomRef = true; + break; + } + } + } + + if (objectHasStringBomRef) + { + object currentRef = null; + PropertyInfo propInfo = null; + if (namedObject is IBomEntityWithRefType_String_BomRef) + { + currentRef = ((IBomEntityWithRefType_String_BomRef)namedObject).GetBomRef(); + } + else + { + propInfo = namedObject.GetType().GetProperty("BomRef", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + namedObject.GetType().Name); + } + currentRef = propInfo.GetValue(namedObject); + } + + if (currentRef.ToString() == oldRef) + { + if (namedObject is IBomEntityWithRefType_String_BomRef) + { + ((IBomEntityWithRefType_String_BomRef)namedObject).SetBomRef(newRef); + } + else + { + propInfo.SetValue(namedObject, newRef); + } + } + else + { + if (currentRef.ToString() != newRef) + { + // Note: "is null" case is also considered an error + throw new BomEntityConflictException("Object listed as having a \"bom-ref\" identifier, but currently its value does not refer to the old name: " + oldRef); + } // else? + } + } + else + { + // TODO: Add handling for other use-cases (if any appear as we evolve) + throw new BomEntityIncompatibleException("Object does not have a \"string BomRef\" property, but was listed as having a \"bom-ref\" identifier: " + oldRef); + } + } + +/* + if (!(namedObjectContainer is null)) + { + bool containerHasStringBomRef = (namedObjectContainer is IBomEntityWithRefType_String_BomRef); + + if (!containerHasStringBomRef) + { + // Slower fallback to facilitate faster code evolution + // (with classes not marked as implementors of interfaces) + if (!(BomEntity.KnownEntityTypeProperties.TryGetValue(namedObjectContainer.GetType(), out PropertyInfo[] props))) + { + props = namedObjectContainer.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + } + + foreach (PropertyInfo propInfo in props) + { + if (propInfo.Name == "BomRef" && propInfo.PropertyType == typeof(string)) + { + containerHasStringBomRef = true; + break; + } + } + } + + if (containerHasStringBomRef) + { + var currentRef = ((IBomEntityWithRefType_String_BomRef)namedObjectContainer).GetBomRef(); + if (currentRef == oldRef) + { + ((IBomEntityWithRefType_String_BomRef)namedObjectContainer).SetBomRef(newRef); + } + else + { + if (currentRef != newRef) + { + // Note: "is null" case is also considered an error + throw new BomEntityConflictException("Object listed as having a \"bom-ref\" identifier, but currently its value does not refer to the old name: " + oldRef); + } // else? + } + } + else + { + // TODO: Add handling for other use-cases (if any appear as we evolve) + throw new BomEntityIncompatibleException("Object does not have a \"string BomRef\" property, but was listed as having a \"bom-ref\" identifier: " + oldRef); + } + } +*/ + + // ...and of back-references (if any): + foreach (var (containedRef, referrerList) in res.dictBackrefs) + { + if (containedRef is null || containedRef != oldRef) + { + continue; + } + + // Check each BomEntity known to refer to this "contained" item's name + foreach (var referrer in referrerList) + { + // Track if we had at least one rename + int referrerModified = 0; + + if (referrer is IBomEntityWithRefLinkType_StringList) + { + // In this class, at least one property is a list of strings + // where some item (maybe several in different lists) contains + // the back-reference of interest. + Dictionary> refLinkConstraints = ((IBomEntityWithRefLinkType_StringList)referrer).GetRefLinkConstraints(_specVersion); + + foreach (var (referrerPropInfo, allowedTypes) in refLinkConstraints) + { + // NOTE: Here we care about properties in referrer + // class that have (are) suitable lists; constraint + // checks are for diligent validation calls, right?.. + Type propType = referrerPropInfo.PropertyType; + if (!(propType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(System.Collections.IList)))) + { + continue; + } + // TODO: Check if the list contents are string? So far + // just assuming so - due to this class interface. + + // Use cached info where available + PropertyInfo listPropCount = null; + MethodInfo listMethodGetItem = null; + MethodInfo listMethodAdd = null; + if (BomEntity.KnownEntityTypeLists.TryGetValue(propType, out BomEntityListReflection refInfo)) + { + listPropCount = refInfo.propCount; + listMethodGetItem = refInfo.methodGetItem; + listMethodAdd = refInfo.methodAdd; + } + else + { + // No cached info about BomEntityListReflection[propType] + listPropCount = propType.GetProperty("Count"); + listMethodGetItem = propType.GetMethod("get_Item"); + listMethodAdd = propType.GetMethod("Add"); + } + + if (listMethodGetItem == null || listPropCount == null || listMethodAdd == null) + { + // Should not have happened, but... + continue; + } + + // Unlike so many other cases around BomEntity, here + // we know the exact expected class at compile time! + // Hope this is a reference to the same list in the + // BomEntity class object, not a copy etc... + List referrerSubList = (List)referrerPropInfo.GetValue(referrer); + if (referrerSubList != null && referrerSubList.Count > 0) + { + // One of string list items should refer the "contained" entity + // There can be only one (ref with this value in this list)... + bool hadHit = false; + + for (int i = 0; i < referrerSubList.Count; i++) + { + if (referrerSubList[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + referrer.GetType() + "." + referrerPropInfo.Name + "[]: " + + oldRef); + } + referrerSubList[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } + } + + +/* + for (int i = 0; i < ((List)referrer).Count; i++) + { + if (((List)referrer)[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException("Multiple references to a \"bom-ref\" identifier detected in the same list of unique items: " + oldRef); + } + ((List)referrer)[i] = newRef; + hadHit = true; + } + } +*/ + } + else + { + // Fallback for a few known classes with lists of refs: + Type referrerType = referrer.GetType(); + if (referrerType == typeof(Composition)) + { + // This contains several lists of strings, and + // at most one of string list items in each of + // those should refer the "contained" entity. + + List referrerSubList = ((Composition)referrer).Assemblies; + if (referrerSubList != null && referrerSubList.Count > 0) + { + bool hadHit = false; + + for (int i = 0; i < referrerSubList.Count; i++) + { + if (referrerSubList[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + "Composition.Assemblies[]: " + oldRef); + } + referrerSubList[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } + + referrerSubList = ((Composition)referrer).Dependencies; + if (referrerSubList != null && referrerSubList.Count > 0) + { + bool hadHit = false; + + for (int i = 0; i < referrerSubList.Count; i++) + { + if (referrerSubList[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + "Composition.Dependencies[]: " + oldRef); + } + referrerSubList[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } + + referrerSubList = ((Composition)referrer).Vulnerabilities; + if (referrerSubList != null && referrerSubList.Count > 0) + { + bool hadHit = false; + + for (int i = 0; i < referrerSubList.Count; i++) + { + if (referrerSubList[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + "Composition.Vulnerabilities[]: " + oldRef); + } + referrerSubList[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } + +/* + if (((Composition)referrer).Assemblies != null && (((Composition)referrer).Assemblies).Count > 0) + { + bool hadHit = false; + + for (int i = 0; i < (((Composition)referrer).Assemblies).Count; i++) + { + if (((List)((Composition)referrer).Assemblies)[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + "Composition.Assemblies[]: " + oldRef); + } + ((List)((Composition)referrer).Assemblies)[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } +*/ + } + + if (referrerType == typeof(EvidenceIdentity)) + { + List referrerSubList = ((EvidenceIdentity)referrer).Tools; + if (referrerSubList != null && referrerSubList.Count > 0) + { + bool hadHit = false; + + for (int i = 0; i < referrerSubList.Count; i++) + { + if (referrerSubList[i] == oldRef) + { + if (hadHit) + { + throw new BomEntityConflictException( + "Multiple references to a \"bom-ref\" identifier detected " + + "in the same list of unique items under " + + "EvidenceIdentity.Tools[]: " + oldRef); + } + referrerSubList[i] = newRef; + hadHit = true; + referrerModified++; + } + } + } + } + } + + // An entity (possibly with a "Ref" property) that directly + // references the "contained" entity. Not an "else" to cater + // for the eventuality that some class would have both some + // list(s) of refs and a "ref" property. + bool referrerHasStringRef = (referrer is IBomEntityWithRefLinkType_String_Ref); + + if (!referrerHasStringRef) + { + // Slower fallback to facilitate faster code evolution + PropertyInfo[] props = + referrer.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly + foreach (var prop in props) + { + if (prop.Name == "Ref" && prop.PropertyType == typeof(string)) + { + referrerHasStringRef = true; + break; + } + } + } + + if (referrerHasStringRef) + { + object currentRef = null; + PropertyInfo propInfo = null; + if (referrer is IBomEntityWithRefLinkType_String_Ref) + { + currentRef = ((IBomEntityWithRefLinkType_String_Ref)referrer).GetRef(); + } + else + { + propInfo = referrer.GetType().GetProperty("Ref", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string Ref\" attribute in class: " + referrer.GetType().Name); + } + currentRef = propInfo.GetValue(referrer); + } + + if (currentRef.ToString() == oldRef) + { + if (referrer is IBomEntityWithRefLinkType_String_Ref) + { + ((IBomEntityWithRefLinkType_String_Ref)referrer).SetRef(newRef); + } + else + { + propInfo.SetValue(referrer, newRef); + } + referrerModified++; + } + else + { + if (currentRef.ToString() == newRef) + { + // We had no conflicts before, so must have achieved + // this via several clones of a referrer?.. + referrerModified++; + } + else + { + throw new BomEntityConflictException("Object listed as having a reference to a \"bom-ref\" identifier, but currently its ref does not refer to the old name: " + oldRef); + } + } + } + else + { + // Was it fixed-up as an object with lists, at least?.. + if (referrerModified == 0) + { + // TODO: Add handling for other use-cases (if any appear as we evolve) + throw new BomEntityIncompatibleException("Object does not have a \"string Ref\" or a suitable list of strings property, but was listed as having a reference to a \"bom-ref\" identifier: " + oldRef); + } + } + } + } + + // Survived without exceptions! ;) + return true; } /// From 8045799c781530daa800346e179af1064940e775 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 04:06:41 +0200 Subject: [PATCH 259/285] Mark suitable classes with IBomEntityWithRefType_String_BomRef Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 2 +- src/CycloneDX.Core/Models/Component.cs | 2 +- src/CycloneDX.Core/Models/Composition.cs | 2 +- src/CycloneDX.Core/Models/Data.cs | 2 +- src/CycloneDX.Core/Models/EvidenceOccurrence.cs | 2 +- src/CycloneDX.Core/Models/Formula.cs | 2 +- src/CycloneDX.Core/Models/License.cs | 2 +- src/CycloneDX.Core/Models/LicenseChoice.cs | 2 +- src/CycloneDX.Core/Models/ModelCard.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalContact.cs | 2 +- src/CycloneDX.Core/Models/OrganizationalEntity.cs | 2 +- src/CycloneDX.Core/Models/Service.cs | 2 +- src/CycloneDX.Core/Models/Trigger.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs | 2 +- src/CycloneDX.Core/Models/Workflow.cs | 2 +- src/CycloneDX.Core/Models/WorkflowTask.cs | 2 +- src/CycloneDX.Core/Models/Workspace.cs | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index 4fc89167..bc88744d 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Annotation : BomEntity + public class Annotation : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlType("subject")] public class XmlAnnotationSubject : BomEntity diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index c992f9c6..db73e1a9 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -29,7 +29,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("component")] [ProtoContract] - public class Component: BomEntity + public class Component: BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum Classification diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index 1c69d215..1a0c1b55 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 : BomEntity, IXmlSerializable + public class Composition : BomEntity, IXmlSerializable, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum AggregateType diff --git a/src/CycloneDX.Core/Models/Data.cs b/src/CycloneDX.Core/Models/Data.cs index 95fc7d7e..5d800533 100644 --- a/src/CycloneDX.Core/Models/Data.cs +++ b/src/CycloneDX.Core/Models/Data.cs @@ -25,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Data : BomEntity + public class Data : BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum DataType diff --git a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs index 32beb5e2..400fab03 100644 --- a/src/CycloneDX.Core/Models/EvidenceOccurrence.cs +++ b/src/CycloneDX.Core/Models/EvidenceOccurrence.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-occurrence")] [ProtoContract] - public class EvidenceOccurrence : BomEntity + public class EvidenceOccurrence : BomEntity, IBomEntityWithRefType_String_BomRef { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Formula.cs b/src/CycloneDX.Core/Models/Formula.cs index 95b4866c..104b7330 100644 --- a/src/CycloneDX.Core/Models/Formula.cs +++ b/src/CycloneDX.Core/Models/Formula.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("formula")] [ProtoContract] - public class Formula : BomEntity + public class Formula : BomEntity, IBomEntityWithRefType_String_BomRef { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/License.cs b/src/CycloneDX.Core/Models/License.cs index 88af7867..6cf10293 100644 --- a/src/CycloneDX.Core/Models/License.cs +++ b/src/CycloneDX.Core/Models/License.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [XmlType("license")] [ProtoContract] - public class License : BomEntity + public class License : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlElement("id")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/LicenseChoice.cs b/src/CycloneDX.Core/Models/LicenseChoice.cs index 75eb5efc..465a7731 100644 --- a/src/CycloneDX.Core/Models/LicenseChoice.cs +++ b/src/CycloneDX.Core/Models/LicenseChoice.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class LicenseChoice : BomEntity + public class LicenseChoice : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlElement("license")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/ModelCard.cs b/src/CycloneDX.Core/Models/ModelCard.cs index 8ce27b92..edcbdda9 100644 --- a/src/CycloneDX.Core/Models/ModelCard.cs +++ b/src/CycloneDX.Core/Models/ModelCard.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("modelCard")] [ProtoContract] - public class ModelCard : BomEntity + public class ModelCard : BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum ModelParameterApproachType diff --git a/src/CycloneDX.Core/Models/OrganizationalContact.cs b/src/CycloneDX.Core/Models/OrganizationalContact.cs index afc5016f..11c7a853 100644 --- a/src/CycloneDX.Core/Models/OrganizationalContact.cs +++ b/src/CycloneDX.Core/Models/OrganizationalContact.cs @@ -22,7 +22,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalContact : BomEntity + public class OrganizationalContact : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/OrganizationalEntity.cs b/src/CycloneDX.Core/Models/OrganizationalEntity.cs index 2831bfdf..42602592 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntity.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntity.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntity : BomEntity + public class OrganizationalEntity : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlElement("name")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 32d18b52..16630f6c 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Service : BomEntity + public class Service : BomEntity, IBomEntityWithRefType_String_BomRef { public Service() { diff --git a/src/CycloneDX.Core/Models/Trigger.cs b/src/CycloneDX.Core/Models/Trigger.cs index 2aa1bd88..7d6c3528 100644 --- a/src/CycloneDX.Core/Models/Trigger.cs +++ b/src/CycloneDX.Core/Models/Trigger.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("trigger")] [ProtoContract] - public class Trigger : BomEntity + public class Trigger : BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum TriggerType diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs index 663cb8c3..d7de6726 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Vulnerability.cs @@ -24,7 +24,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Vulnerability : BomEntity + public class Vulnerability : BomEntity, IBomEntityWithRefType_String_BomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Workflow.cs b/src/CycloneDX.Core/Models/Workflow.cs index ed52c85c..40439f13 100644 --- a/src/CycloneDX.Core/Models/Workflow.cs +++ b/src/CycloneDX.Core/Models/Workflow.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workflow")] [ProtoContract] - public class Workflow : BomEntity + public class Workflow : BomEntity, IBomEntityWithRefType_String_BomRef { [JsonPropertyName("bom-ref")] [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/WorkflowTask.cs b/src/CycloneDX.Core/Models/WorkflowTask.cs index bf2efeb6..8e089634 100644 --- a/src/CycloneDX.Core/Models/WorkflowTask.cs +++ b/src/CycloneDX.Core/Models/WorkflowTask.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("task")] [ProtoContract] - public class WorkflowTask : BomEntity + public class WorkflowTask : BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum TaskType diff --git a/src/CycloneDX.Core/Models/Workspace.cs b/src/CycloneDX.Core/Models/Workspace.cs index d9fac743..23931eef 100644 --- a/src/CycloneDX.Core/Models/Workspace.cs +++ b/src/CycloneDX.Core/Models/Workspace.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("workspace")] [ProtoContract] - public class Workspace : BomEntity + public class Workspace : BomEntity, IBomEntityWithRefType_String_BomRef { [ProtoContract] public enum AccessModeType From a9d56a1767a2151a90bbc00e6c346172e302906d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 04:26:21 +0200 Subject: [PATCH 260/285] Mark suitable classes with IBomEntityWithRefLinkType_String_Ref and IBomEntityWithRefLinkType_StringList Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 4 +++- src/CycloneDX.Core/Models/Composition.cs | 2 +- src/CycloneDX.Core/Models/DatasetChoice.cs | 2 +- src/CycloneDX.Core/Models/Dependency.cs | 2 +- src/CycloneDX.Core/Models/EvidenceIdentity.cs | 2 +- src/CycloneDX.Core/Models/ResourceReferenceChoice.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index bc88744d..017beced 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -25,9 +25,11 @@ namespace CycloneDX.Models { [ProtoContract] public class Annotation : BomEntity, IBomEntityWithRefType_String_BomRef + // NOTE: *Not* IBomEntityWithRefLinkType_StringList due + // to inlaid "subject" property type with dedicated class { [XmlType("subject")] - public class XmlAnnotationSubject : BomEntity + public class XmlAnnotationSubject : BomEntity, IBomEntityWithRefLinkType_String_Ref { [XmlAttribute("ref")] public string Ref { get; set; } diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index 1a0c1b55..f3b5437d 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 : BomEntity, IXmlSerializable, IBomEntityWithRefType_String_BomRef + public class Composition : BomEntity, IXmlSerializable, IBomEntityWithRefType_String_BomRef, IBomEntityWithRefLinkType_StringList { [ProtoContract] public enum AggregateType diff --git a/src/CycloneDX.Core/Models/DatasetChoice.cs b/src/CycloneDX.Core/Models/DatasetChoice.cs index ddae69cf..f332b9ce 100644 --- a/src/CycloneDX.Core/Models/DatasetChoice.cs +++ b/src/CycloneDX.Core/Models/DatasetChoice.cs @@ -21,7 +21,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DatasetChoice : BomEntity + public class DatasetChoice : BomEntity, IBomEntityWithRefLinkType_String_Ref { [XmlElement("dataset")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 15471381..93077157 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [XmlType("dependency")] [ProtoContract] - public class Dependency : BomEntity + public class Dependency : BomEntity, IBomEntityWithRefLinkType_String_Ref { [XmlAttribute("ref")] [ProtoMember(1)] diff --git a/src/CycloneDX.Core/Models/EvidenceIdentity.cs b/src/CycloneDX.Core/Models/EvidenceIdentity.cs index 82cce450..ad73ef73 100644 --- a/src/CycloneDX.Core/Models/EvidenceIdentity.cs +++ b/src/CycloneDX.Core/Models/EvidenceIdentity.cs @@ -28,7 +28,7 @@ namespace CycloneDX.Models { [XmlType("evidence-identity")] [ProtoContract] - public class EvidenceIdentity : BomEntity + public class EvidenceIdentity : BomEntity, IBomEntityWithRefLinkType_StringList { [ProtoContract] public enum EvidenceFieldType diff --git a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs index 55451d48..1d4e862b 100644 --- a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs +++ b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class ResourceReferenceChoice : BomEntity, IXmlSerializable + public class ResourceReferenceChoice : BomEntity, IXmlSerializable, IBomEntityWithRefLinkType_String_Ref { private static XmlSerializer _extRefSerializer; private static XmlSerializer GetExternalReferenceSerializer() diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs index ef154df0..3f529ad8 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models.Vulnerabilities { [ProtoContract] - public class Affects : BomEntity + public class Affects : BomEntity, IBomEntityWithRefLinkType_String_Ref { [XmlElement("ref")] [ProtoMember(1)] From f588a4e1f5a8a09ade7537138d52eeaaa7ff4a12 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 14:24:50 +0200 Subject: [PATCH 261/285] Implement GetRefLinkConstraints() for classes marked with IBomEntityWithRefLinkType* interfaces Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 19 +++++++++ src/CycloneDX.Core/Models/Bom.cs | 4 +- src/CycloneDX.Core/Models/BomEntity.cs | 20 ++++++++- src/CycloneDX.Core/Models/Composition.cs | 37 ++++++++++++++++ src/CycloneDX.Core/Models/DatasetChoice.cs | 21 ++++++++++ src/CycloneDX.Core/Models/Dependency.cs | 42 +++++++++++++++++++ src/CycloneDX.Core/Models/EvidenceIdentity.cs | 20 +++++++++ .../Models/ResourceReferenceChoice.cs | 21 ++++++++++ .../Models/Vulnerabilities/Affects.cs | 27 ++++++++++++ 9 files changed, 209 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index 017beced..ef722738 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -15,8 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; using System.Xml.Serialization; using System.Text.Json.Serialization; using ProtoBuf; @@ -33,6 +36,22 @@ public class XmlAnnotationSubject : BomEntity, IBomEntityWithRefLinkType_String_ { [XmlAttribute("ref")] public string Ref { get; set; } + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_AnyBomEntity = + new Dictionary>() + { + { typeof(XmlAnnotationSubject).GetProperty("Ref", typeof(string)), RefLinkConstraints_AnyBomEntity } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + // TODO: switch/case for CDX spec newer than 1.5 where this type got introduced + if (specificationVersion == v1_5) + { + return RefLinkConstraints_StringRef_AnyBomEntity; + } + return null; + } } [XmlAttribute("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index c0bb0229..79ab4aa1 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -644,7 +645,8 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) // In this class, at least one property is a list of strings // where some item (maybe several in different lists) contains // the back-reference of interest. - Dictionary> refLinkConstraints = ((IBomEntityWithRefLinkType_StringList)referrer).GetRefLinkConstraints(_specVersion); + ImmutableDictionary> refLinkConstraints = + ((IBomEntityWithRefLinkType_StringList)referrer).GetRefLinkConstraints(_specVersion); foreach (var (referrerPropInfo, allowedTypes) in refLinkConstraints) { diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 85055906..2df49c34 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -512,7 +512,7 @@ public interface IBomEntityWithRefLinkType : IBomEntity // better? Would it be faster in processing // (with reflection) e.g. to *find* which // properties to look at? - public Dictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion); + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion); } /// @@ -860,6 +860,24 @@ public class BomEntity : IBomEntity return ImmutableDictionary.CreateRange(dict); }) (); + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_AnyBomEntity = new List() {typeof(CycloneDX.Models.BomEntity)}.ToImmutableList(); + + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_Component = new List() {typeof(CycloneDX.Models.Component)}.ToImmutableList(); + + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_Service = new List() {typeof(CycloneDX.Models.Service)}.ToImmutableList(); + + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_ComponentOrService = new List() {typeof(CycloneDX.Models.Component), typeof(CycloneDX.Models.Service)}.ToImmutableList(); + + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_ModelDataset = new List() {typeof(CycloneDX.Models.Data)}.ToImmutableList(); + + /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. + public static readonly ImmutableList RefLinkConstraints_Vulnerability = new List() {typeof(CycloneDX.Models.Vulnerabilities.Vulnerability)}.ToImmutableList(); + protected BomEntity() { // a bad alternative to private could be to: throw new NotImplementedException("The BomEntity class directly should not be instantiated") diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index f3b5437d..24254951 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -15,8 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; using System.Xml; using System.Xml.Serialization; using System.Text.Json.Serialization; @@ -211,5 +214,39 @@ public static void NormalizeList(bool ascending, bool recursive, List (o?.Aggregate, o?.Assemblies, o?.Dependencies), null); } + + private static readonly ImmutableDictionary> RefLinkConstraints_List_v1_3 = + new Dictionary>() + { + { typeof(Composition).GetProperty("Assemblies", typeof(List)), RefLinkConstraints_ComponentOrService }, + { typeof(Composition).GetProperty("Dependencies", typeof(List)), RefLinkConstraints_ComponentOrService } + }.ToImmutableDictionary(); + + private static readonly ImmutableDictionary> RefLinkConstraints_List_v1_5 = + new Dictionary>() + { + { typeof(Composition).GetProperty("Assemblies", typeof(List)), RefLinkConstraints_ComponentOrService }, + { typeof(Composition).GetProperty("Dependencies", typeof(List)), RefLinkConstraints_ComponentOrService }, + { typeof(Composition).GetProperty("Vulnerabilities", typeof(List)), RefLinkConstraints_Vulnerability } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + switch (specificationVersion) + { + case v1_0: + case v1_1: + case v1_2: + return null; + + case v1_3: + case v1_4: + return RefLinkConstraints_List_v1_3; + + case v1_5: + default: + return RefLinkConstraints_List_v1_5; + } + } } } diff --git a/src/CycloneDX.Core/Models/DatasetChoice.cs b/src/CycloneDX.Core/Models/DatasetChoice.cs index f332b9ce..a016efd7 100644 --- a/src/CycloneDX.Core/Models/DatasetChoice.cs +++ b/src/CycloneDX.Core/Models/DatasetChoice.cs @@ -15,6 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; using System.Xml.Serialization; using ProtoBuf; @@ -30,5 +35,21 @@ public class DatasetChoice : BomEntity, IBomEntityWithRefLinkType_String_Ref [XmlElement("ref")] [ProtoMember(2)] public string Ref { get; set; } + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ModelDataset = + new Dictionary>() + { + { typeof(DatasetChoice).GetProperty("Ref", typeof(string)), RefLinkConstraints_ModelDataset } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + // TODO: switch/case for CDX spec newer than 1.5 where this type got introduced + if (specificationVersion == v1_5) + { + return RefLinkConstraints_StringRef_ModelDataset; + } + return null; + } } } diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 93077157..1075bf9c 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -15,9 +15,12 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Collections.Immutable; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -52,5 +55,44 @@ public static void NormalizeList(bool ascending, bool recursive, List (o?.Ref), null); } + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_Component = + new Dictionary>() + { + { typeof(Dependency).GetProperty("Ref", typeof(string)), RefLinkConstraints_Component } + }.ToImmutableDictionary(); + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ComponentOrService = + new Dictionary>() + { + { typeof(Dependency).GetProperty("Ref", typeof(string)), RefLinkConstraints_ComponentOrService } + }.ToImmutableDictionary(); + + /// + /// See IBomEntityWithRefLinkType.GetRefLinkConstraints(). + /// + /// + /// + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + switch (specificationVersion) + { + case v1_0: + case v1_1: + return null; + + case v1_2: + case v1_3: + case v1_4: + // NOTE: XML and JSON schema descriptions differ: + // * in JSON, specs v1.2, 1.3 and 1.4 dealt with "components" + // * in XML since 1.2, and in JSON since 1.5, with "components or services" + //TOTHINK//return RefLinkConstraints_StringRef_Component?.. + + case v1_5: + default: + return RefLinkConstraints_StringRef_ComponentOrService; + } + } } } diff --git a/src/CycloneDX.Core/Models/EvidenceIdentity.cs b/src/CycloneDX.Core/Models/EvidenceIdentity.cs index ad73ef73..c76d4192 100644 --- a/src/CycloneDX.Core/Models/EvidenceIdentity.cs +++ b/src/CycloneDX.Core/Models/EvidenceIdentity.cs @@ -15,10 +15,13 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml; using System.Xml.Serialization; @@ -67,5 +70,22 @@ public enum EvidenceFieldType [XmlElement("tools")] [ProtoMember(4)] public EvidenceTools Tools { get; set; } + + private static readonly ImmutableDictionary> RefLinkConstraints_List_AnyBomEntity = + new Dictionary>() + { + // EvidenceTools is a List as of CDX spec 1.5 + { typeof(EvidenceIdentity).GetProperty("Tools", typeof(EvidenceTools)), RefLinkConstraints_AnyBomEntity } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + // TODO: switch/case for CDX spec newer than 1.5 where this type got introduced + if (specificationVersion == v1_5) + { + return RefLinkConstraints_List_AnyBomEntity; + } + return null; + } } } \ No newline at end of file diff --git a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs index 1d4e862b..daeab8ec 100644 --- a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs +++ b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs @@ -15,6 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml; using System.Xml.Serialization; @@ -72,5 +77,21 @@ public void WriteXml(XmlWriter writer) { GetExternalReferenceSerializer().Serialize(writer, this.ExternalReference); } } + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_AnyBomEntity = + new Dictionary>() + { + { typeof(ResourceReferenceChoice).GetProperty("Ref", typeof(string)), RefLinkConstraints_AnyBomEntity } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + // TODO: switch/case for CDX spec newer than 1.5 where this type got introduced + if (specificationVersion == v1_5) + { + return RefLinkConstraints_StringRef_AnyBomEntity; + } + return null; + } } } diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs index 3f529ad8..8e7a9fa3 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs @@ -15,7 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using static CycloneDX.SpecificationVersion; +using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; using System.Text.Json.Serialization; using System.Xml.Serialization; using ProtoBuf; @@ -34,5 +38,28 @@ public class Affects : BomEntity, IBomEntityWithRefLinkType_String_Ref [JsonPropertyName("versions")] [ProtoMember(2)] public List Versions { get; set; } + + private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ComponentOrService = + new Dictionary>() + { + { typeof(Affects).GetProperty("Ref", typeof(string)), RefLinkConstraints_ComponentOrService } + }.ToImmutableDictionary(); + + public ImmutableDictionary> GetRefLinkConstraints(SpecificationVersion specificationVersion) + { + switch (specificationVersion) + { + case v1_0: + case v1_1: + case v1_2: + case v1_3: + return null; + + case v1_4: + case v1_5: + default: + return RefLinkConstraints_StringRef_ComponentOrService; + } + } } } From e2473cca70a049317ea865656e465956febfaa19 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 14:26:26 +0200 Subject: [PATCH 262/285] Bom.cs, BomEntity.cs: common base-class default implementation of getter/setter methods Could not just declare in interface anticipated getter/setter methods (for zero copy-pasta) - that conflicted with the later generated getters/setter actual methods. Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 4 +- src/CycloneDX.Core/Models/BomEntity.cs | 76 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 79ab4aa1..b5a28d73 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -467,7 +467,9 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) // First check if there is anything to rename, and if the name is // already known as somebody's identifier. Dictionary dictBomrefs = res.GetBomRefsWithContainer(); - // At most we have one(!) object with "oldRef" name as its identifier: + + // At most we have one(!) object with "oldRef" name as its identifier + // (stored as a property of this object): BomEntity namedObject = null; BomEntity namedObjectContainer = null; foreach (var (contained, container) in dictBomrefs) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 2df49c34..a27ecbda 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1255,6 +1255,82 @@ public bool MergeWith(BomEntity obj, BomEntityListMergeHelperStrategy listMergeH "Base-method implementation treats equivalent but not equal entities as conflicting", this.GetType()); } + + /// + /// Default implementation for derived classes which implement IBomEntityWithRefType + /// + /// + public string GetBomRef() + { + if (this is IBomEntityWithRefType) + { + Type thisType = this.GetType(); + PropertyInfo propInfo = thisType.GetProperty("BomRef", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + thisType.Name); + } + return (string)propInfo.GetValue(this); + } + + return null; + } + + /// + /// Default implementation for derived classes which implement IBomEntityWithRefType + /// + /// + public void SetBomRef(string s) + { + if (this is IBomEntityWithRefType) + { + Type thisType = this.GetType(); + PropertyInfo propInfo = thisType.GetProperty("BomRef", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + thisType.Name); + } + propInfo.SetValue(this, s); + } + } + + /// + /// Default implementation for derived classes which implement IBomEntityWithRefLinkType + /// + /// + public string GetRef() + { + if (this is IBomEntityWithRefLinkType) + { + Type thisType = this.GetType(); + PropertyInfo propInfo = thisType.GetProperty("Ref", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string Ref\" attribute in class: " + thisType.Name); + } + return (string)propInfo.GetValue(this); + } + + return null; + } + + /// + /// Default implementation for derived classes which implement IBomEntityWithRefLinkType + /// + /// + public void SetRef(string s) + { + if (this is IBomEntityWithRefLinkType) + { + Type thisType = this.GetType(); + PropertyInfo propInfo = thisType.GetProperty("Ref", typeof(string)); + if (propInfo is null) + { + throw new BomEntityIncompatibleException("No \"string Ref\" attribute in class: " + thisType.Name); + } + propInfo.SetValue(this, s); + } + } } /// From f5046ab57271dbaef048166aae18b918845e728a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 5 Oct 2023 14:52:06 +0200 Subject: [PATCH 263/285] Bom.cs: drop commented-away code about RenameBomRef() alternative experiments Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 88 -------------------------------- 1 file changed, 88 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index b5a28d73..6fbb03a6 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -580,54 +580,6 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } } -/* - if (!(namedObjectContainer is null)) - { - bool containerHasStringBomRef = (namedObjectContainer is IBomEntityWithRefType_String_BomRef); - - if (!containerHasStringBomRef) - { - // Slower fallback to facilitate faster code evolution - // (with classes not marked as implementors of interfaces) - if (!(BomEntity.KnownEntityTypeProperties.TryGetValue(namedObjectContainer.GetType(), out PropertyInfo[] props))) - { - props = namedObjectContainer.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // BindingFlags.DeclaredOnly - } - - foreach (PropertyInfo propInfo in props) - { - if (propInfo.Name == "BomRef" && propInfo.PropertyType == typeof(string)) - { - containerHasStringBomRef = true; - break; - } - } - } - - if (containerHasStringBomRef) - { - var currentRef = ((IBomEntityWithRefType_String_BomRef)namedObjectContainer).GetBomRef(); - if (currentRef == oldRef) - { - ((IBomEntityWithRefType_String_BomRef)namedObjectContainer).SetBomRef(newRef); - } - else - { - if (currentRef != newRef) - { - // Note: "is null" case is also considered an error - throw new BomEntityConflictException("Object listed as having a \"bom-ref\" identifier, but currently its value does not refer to the old name: " + oldRef); - } // else? - } - } - else - { - // TODO: Add handling for other use-cases (if any appear as we evolve) - throw new BomEntityIncompatibleException("Object does not have a \"string BomRef\" property, but was listed as having a \"bom-ref\" identifier: " + oldRef); - } - } -*/ - // ...and of back-references (if any): foreach (var (containedRef, referrerList) in res.dictBackrefs) { @@ -717,22 +669,6 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } } } - - -/* - for (int i = 0; i < ((List)referrer).Count; i++) - { - if (((List)referrer)[i] == oldRef) - { - if (hadHit) - { - throw new BomEntityConflictException("Multiple references to a \"bom-ref\" identifier detected in the same list of unique items: " + oldRef); - } - ((List)referrer)[i] = newRef; - hadHit = true; - } - } -*/ } else { @@ -812,30 +748,6 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } } } - -/* - if (((Composition)referrer).Assemblies != null && (((Composition)referrer).Assemblies).Count > 0) - { - bool hadHit = false; - - for (int i = 0; i < (((Composition)referrer).Assemblies).Count; i++) - { - if (((List)((Composition)referrer).Assemblies)[i] == oldRef) - { - if (hadHit) - { - throw new BomEntityConflictException( - "Multiple references to a \"bom-ref\" identifier detected " + - "in the same list of unique items under " + - "Composition.Assemblies[]: " + oldRef); - } - ((List)((Composition)referrer).Assemblies)[i] = newRef; - hadHit = true; - referrerModified++; - } - } - } -*/ } if (referrerType == typeof(EvidenceIdentity)) From 4b8e1ae74ec80030072bbaab8552d5045dd54f74 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:03:00 +0200 Subject: [PATCH 264/285] Address Codacy complaints: redundant parentheses in new Dict<...>() {...} Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Annotation.cs | 2 +- src/CycloneDX.Core/Models/Composition.cs | 4 ++-- src/CycloneDX.Core/Models/DatasetChoice.cs | 2 +- src/CycloneDX.Core/Models/Dependency.cs | 4 ++-- src/CycloneDX.Core/Models/ResourceReferenceChoice.cs | 2 +- src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CycloneDX.Core/Models/Annotation.cs b/src/CycloneDX.Core/Models/Annotation.cs index ef722738..2f623421 100644 --- a/src/CycloneDX.Core/Models/Annotation.cs +++ b/src/CycloneDX.Core/Models/Annotation.cs @@ -38,7 +38,7 @@ public class XmlAnnotationSubject : BomEntity, IBomEntityWithRefLinkType_String_ public string Ref { get; set; } private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_AnyBomEntity = - new Dictionary>() + new Dictionary> { { typeof(XmlAnnotationSubject).GetProperty("Ref", typeof(string)), RefLinkConstraints_AnyBomEntity } }.ToImmutableDictionary(); diff --git a/src/CycloneDX.Core/Models/Composition.cs b/src/CycloneDX.Core/Models/Composition.cs index 24254951..5170db14 100644 --- a/src/CycloneDX.Core/Models/Composition.cs +++ b/src/CycloneDX.Core/Models/Composition.cs @@ -216,14 +216,14 @@ public static void NormalizeList(bool ascending, bool recursive, List> RefLinkConstraints_List_v1_3 = - new Dictionary>() + new Dictionary> { { typeof(Composition).GetProperty("Assemblies", typeof(List)), RefLinkConstraints_ComponentOrService }, { typeof(Composition).GetProperty("Dependencies", typeof(List)), RefLinkConstraints_ComponentOrService } }.ToImmutableDictionary(); private static readonly ImmutableDictionary> RefLinkConstraints_List_v1_5 = - new Dictionary>() + new Dictionary> { { typeof(Composition).GetProperty("Assemblies", typeof(List)), RefLinkConstraints_ComponentOrService }, { typeof(Composition).GetProperty("Dependencies", typeof(List)), RefLinkConstraints_ComponentOrService }, diff --git a/src/CycloneDX.Core/Models/DatasetChoice.cs b/src/CycloneDX.Core/Models/DatasetChoice.cs index a016efd7..32e8d95a 100644 --- a/src/CycloneDX.Core/Models/DatasetChoice.cs +++ b/src/CycloneDX.Core/Models/DatasetChoice.cs @@ -37,7 +37,7 @@ public class DatasetChoice : BomEntity, IBomEntityWithRefLinkType_String_Ref public string Ref { get; set; } private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ModelDataset = - new Dictionary>() + new Dictionary> { { typeof(DatasetChoice).GetProperty("Ref", typeof(string)), RefLinkConstraints_ModelDataset } }.ToImmutableDictionary(); diff --git a/src/CycloneDX.Core/Models/Dependency.cs b/src/CycloneDX.Core/Models/Dependency.cs index 1075bf9c..a1b9fb64 100644 --- a/src/CycloneDX.Core/Models/Dependency.cs +++ b/src/CycloneDX.Core/Models/Dependency.cs @@ -57,13 +57,13 @@ public static void NormalizeList(bool ascending, bool recursive, List> RefLinkConstraints_StringRef_Component = - new Dictionary>() + new Dictionary> { { typeof(Dependency).GetProperty("Ref", typeof(string)), RefLinkConstraints_Component } }.ToImmutableDictionary(); private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ComponentOrService = - new Dictionary>() + new Dictionary> { { typeof(Dependency).GetProperty("Ref", typeof(string)), RefLinkConstraints_ComponentOrService } }.ToImmutableDictionary(); diff --git a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs index daeab8ec..1cf78e56 100644 --- a/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs +++ b/src/CycloneDX.Core/Models/ResourceReferenceChoice.cs @@ -79,7 +79,7 @@ public void WriteXml(XmlWriter writer) { } private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_AnyBomEntity = - new Dictionary>() + new Dictionary> { { typeof(ResourceReferenceChoice).GetProperty("Ref", typeof(string)), RefLinkConstraints_AnyBomEntity } }.ToImmutableDictionary(); diff --git a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs index 8e7a9fa3..dd4b6b30 100644 --- a/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs +++ b/src/CycloneDX.Core/Models/Vulnerabilities/Affects.cs @@ -40,7 +40,7 @@ public class Affects : BomEntity, IBomEntityWithRefLinkType_String_Ref public List Versions { get; set; } private static readonly ImmutableDictionary> RefLinkConstraints_StringRef_ComponentOrService = - new Dictionary>() + new Dictionary> { { typeof(Affects).GetProperty("Ref", typeof(string)), RefLinkConstraints_ComponentOrService } }.ToImmutableDictionary(); From c9443246a63b1f9cc5469adeece8453f240ccca4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:05:45 +0200 Subject: [PATCH 265/285] Address Codacy complaints: redundant parentheses in new List<...>() {...} Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index a27ecbda..3a164d01 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -861,22 +861,22 @@ public class BomEntity : IBomEntity }) (); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_AnyBomEntity = new List() {typeof(CycloneDX.Models.BomEntity)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_AnyBomEntity = new List {typeof(CycloneDX.Models.BomEntity)}.ToImmutableList(); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_Component = new List() {typeof(CycloneDX.Models.Component)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_Component = new List {typeof(CycloneDX.Models.Component)}.ToImmutableList(); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_Service = new List() {typeof(CycloneDX.Models.Service)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_Service = new List {typeof(CycloneDX.Models.Service)}.ToImmutableList(); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_ComponentOrService = new List() {typeof(CycloneDX.Models.Component), typeof(CycloneDX.Models.Service)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_ComponentOrService = new List {typeof(CycloneDX.Models.Component), typeof(CycloneDX.Models.Service)}.ToImmutableList(); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_ModelDataset = new List() {typeof(CycloneDX.Models.Data)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_ModelDataset = new List {typeof(CycloneDX.Models.Data)}.ToImmutableList(); /// Used by IBomEntityWithRefLinkType.GetRefLinkConstraints() in some descendant classes. - public static readonly ImmutableList RefLinkConstraints_Vulnerability = new List() {typeof(CycloneDX.Models.Vulnerabilities.Vulnerability)}.ToImmutableList(); + public static readonly ImmutableList RefLinkConstraints_Vulnerability = new List {typeof(CycloneDX.Models.Vulnerabilities.Vulnerability)}.ToImmutableList(); protected BomEntity() { From e6f60707db45af341f07194d9f9f80d67da54bdd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:12:47 +0200 Subject: [PATCH 266/285] Address Codacy complaints: make use of namedObjectContainer, convert some messages to formatting strings, wrap longer others Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 6fbb03a6..94f6621c 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -486,7 +486,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) var propInfo = contained.GetType().GetProperty("BomRef", typeof(string)); if (propInfo is null) { - throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + contained.GetType().Name); + throw new BomEntityIncompatibleException($"No \"string BomRef\" attribute in class: {contained.GetType().Name}"); } containedBomRef = propInfo.GetValue(contained); } @@ -495,7 +495,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) { if (namedObject != null) { - throw new BomEntityConflictException("Duplicate \"bom-ref\" identifier detected in Bom document: " + oldRef); + throw new BomEntityConflictException($"Duplicate \"bom-ref\" identifier detected in Bom document, previously under a BomEntity typed {namedObjectContainer.GetType().Name}: {oldRef}"); } namedObject = contained; namedObjectContainer = container; @@ -504,7 +504,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) if (containedBomRef.ToString() == newRef) { - throw new ArgumentException("newRef is already used to name a BomEntity: " + newRef); + throw new ArgumentException($"newRef is already used to name a BomEntity: {newRef}"); } } @@ -548,7 +548,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) propInfo = namedObject.GetType().GetProperty("BomRef", typeof(string)); if (propInfo is null) { - throw new BomEntityIncompatibleException("No \"string BomRef\" attribute in class: " + namedObject.GetType().Name); + throw new BomEntityIncompatibleException($"No \"string BomRef\" attribute in class: {namedObject.GetType().Name}"); } currentRef = propInfo.GetValue(namedObject); } @@ -569,14 +569,14 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) if (currentRef.ToString() != newRef) { // Note: "is null" case is also considered an error - throw new BomEntityConflictException("Object listed as having a \"bom-ref\" identifier, but currently its value does not refer to the old name: " + oldRef); + throw new BomEntityConflictException($"Object listed as having a \"bom-ref\" identifier, but currently its value does not refer to the old name: {oldRef}"); } // else? } } else { // TODO: Add handling for other use-cases (if any appear as we evolve) - throw new BomEntityIncompatibleException("Object does not have a \"string BomRef\" property, but was listed as having a \"bom-ref\" identifier: " + oldRef); + throw new BomEntityIncompatibleException($"Object does not have a \"string BomRef\" property, but was listed as having a \"bom-ref\" identifier: {oldRef}"); } } @@ -811,7 +811,9 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) propInfo = referrer.GetType().GetProperty("Ref", typeof(string)); if (propInfo is null) { - throw new BomEntityIncompatibleException("No \"string Ref\" attribute in class: " + referrer.GetType().Name); + throw new BomEntityIncompatibleException( + "No \"string Ref\" attribute in class: " + + referrer.GetType().Name); } currentRef = propInfo.GetValue(referrer); } @@ -838,7 +840,9 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } else { - throw new BomEntityConflictException("Object listed as having a reference to a \"bom-ref\" identifier, but currently its ref does not refer to the old name: " + oldRef); + throw new BomEntityConflictException( + "Object listed as having a reference to a \"bom-ref\" identifier, " + + "but currently its ref does not refer to the old name: " + oldRef); } } } @@ -848,7 +852,10 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) if (referrerModified == 0) { // TODO: Add handling for other use-cases (if any appear as we evolve) - throw new BomEntityIncompatibleException("Object does not have a \"string Ref\" or a suitable list of strings property, but was listed as having a reference to a \"bom-ref\" identifier: " + oldRef); + throw new BomEntityIncompatibleException( + "Object does not have a \"string Ref\" or a suitable " + + "list of strings property, but was listed as having " + + "a reference to a \"bom-ref\" identifier: " + oldRef); } } } From 92a5e26cc3fe4de3f6488aa8900bfce786f44077 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:15:38 +0200 Subject: [PATCH 267/285] Address Codacy complaints: make use of referrerModified for return value, as planned Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 94f6621c..2e901a45 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -862,7 +862,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } // Survived without exceptions! ;) - return true; + return (referrerModified == 0); } /// From 7a494668656e97280fbf3e51cea8f5c814de15e6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:38:45 +0200 Subject: [PATCH 268/285] Address Codacy complaints: convert some properties to private with getter/setter; avoid initializing to default value du-jour Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 3a164d01..e62a8741 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1342,7 +1342,7 @@ public class BomWalkResult /// The BomEntity (normally a whole Bom document) /// which was walked and reported here. /// - public BomEntity bomRoot = null; + private BomEntity bomRoot { get; set; } /// /// Populated by GetBomRefsInContainers(), @@ -1368,7 +1368,7 @@ public class BomWalkResult // (and printing in ToString() method) to help // debug the data-walk overheads. Accounting // does have a cost (~5% for a larger 20s run). - public bool debugPerformance = false; + private bool debugPerformance { get; set; } // Helpers for performance accounting - how hard // was it to discover the information in this @@ -1392,8 +1392,9 @@ public class BomWalkResult private int sbeCountNewBomRefCheckDict { get; set; } private int sbeCountNewBomRef { get; set; } - // This one is null, outermost loop makes a new instance, starts and stops it: - private Stopwatch stopWatchWalkTotal = null; + // This one is initially null: the outermost walk loop + // makes a new instance, starts and stops this stopwatch + private Stopwatch stopWatchWalkTotal; private Stopwatch stopWatchEvalAttr = new Stopwatch(); private Stopwatch stopWatchNewBomref = new Stopwatch(); private Stopwatch stopWatchNewBomrefCheck = new Stopwatch(); From 0a530f3974714662a0edd0ae09ea56526f8c44f6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:38:56 +0200 Subject: [PATCH 269/285] Address Codacy complaints: code example is not commented-away code (add pragma for Sonar) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index e62a8741..647019d4 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1843,6 +1843,8 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) sbeCountNewBomRefCheckDict++; stopWatchNewBomrefCheck.Start(); } + + #pragma warning disable S125 // "proper" dict key lookup probably goes via hashes // which go via serialization for BomEntity classes, // and so walking a Bom with a hundred Components @@ -1855,6 +1857,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) // this should not happen in this loop, and the // intention is to keep tabs on references to all // original objects so we can rename what we need): + #pragma warning restore S125 foreach (var (cont, list) in dictRefsInContainers) { if (Object.ReferenceEquals(container, cont)) From a06d4652e0e9e8f982ee3cc26a17c388fcd7b68f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:39:12 +0200 Subject: [PATCH 270/285] Address Codacy complaints: avoid needless cast Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 647019d4..910bc414 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1896,7 +1896,7 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) sbeCountNewBomRef++; stopWatchNewBomrefListAdd.Start(); } - containerList.Add((BomEntity)obj); + containerList.Add(obj); if (debugPerformance) { stopWatchNewBomrefListAdd.Stop(); From c911bd46c1e35822ebd5a88c1db3e73776c889bc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Oct 2023 23:45:53 +0200 Subject: [PATCH 271/285] BomEntity.cs: add GetRefsInContainers() for completeness (essentially returns dictBackrefs) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 910bc414..51fa5deb 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -2116,6 +2116,25 @@ public Dictionary> GetBomRefsInContainers() return dictRefsInContainers; } + /// + /// Provide a Dictionary whose keys are "Ref" or equivalent + /// string values which link back to a "BomRef" hopefully + /// defined somewhere in the same Bom document (but may be + /// dangling, or sometimes co-opted with external links to + /// other Bom documents!), and whose values are lists of + /// BomEntities which use this same "ref" value. + /// + /// See also: GetBomRefsInContainers() with similar info + /// about keys which are BomEntity "containers" and values + /// are lists of BomEntity with a BomRef in those containers, + /// and GetBomRefsWithContainer() with transposed returns. + /// + /// + public Dictionary> GetRefsInContainers() + { + return dictBackrefs; + } + /// /// Provide a Dictionary whose keys are "contained" entities /// with a BomRef attribute and values are their direct From c2e850dc01d2eb089f41dcd8bea500452504fe4f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:53:02 +0200 Subject: [PATCH 272/285] Address Codacy complaints: take BomWalkResult.dictBackrefs private Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 2 +- src/CycloneDX.Core/Models/BomEntity.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 2e901a45..c9ac1e5d 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -581,7 +581,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) } // ...and of back-references (if any): - foreach (var (containedRef, referrerList) in res.dictBackrefs) + foreach (var (containedRef, referrerList) in res.GetRefsInContainers()) { if (containedRef is null || containedRef != oldRef) { diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 51fa5deb..afe15875 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1361,8 +1361,9 @@ public class BomWalkResult /// with external links to other Bom documents!), /// and values are lists of entities which use /// this same "ref" value. + /// Exposed by GetRefsInContainers(). /// - readonly public Dictionary> dictBackrefs = new Dictionary>(); + readonly private Dictionary> dictBackrefs = new Dictionary>(); // Callers can enable performance monitoring // (and printing in ToString() method) to help From 575c505330b7a8cf337a8b643d730ebc8a31dedf Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:54:30 +0200 Subject: [PATCH 273/285] Address Codacy complaints: take BomWalkResult.dictRefsInContainers private Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 2 +- src/CycloneDX.Core/Models/BomEntity.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index c9ac1e5d..c2b92434 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -371,7 +371,7 @@ private void AssertThisBomWalkResult(BomWalkResult res) public Dictionary> GetBomRefsInContainers(BomWalkResult res) { AssertThisBomWalkResult(res); - return res.dictRefsInContainers; + return res.GetBomRefsInContainers(); } /// diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index afe15875..5a6abfe5 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1349,8 +1349,9 @@ public class BomWalkResult /// keys are "container" entities and values /// are lists of "contained" entities which /// have a BomRef or equivalent property. + /// Exposed by GetBomRefsInContainers(). /// - readonly public Dictionary> dictRefsInContainers = new Dictionary>(); + readonly private Dictionary> dictRefsInContainers = new Dictionary>(); /// /// Populated by GetBomRefsInContainers(), From 21487a02e5e2623631f73b2a287cc491343bbe8a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 12:59:04 +0200 Subject: [PATCH 274/285] Bom.cs: add GetRefsInContainers() for completeness Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index c2b92434..a7607d04 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -349,6 +349,38 @@ private void AssertThisBomWalkResult(BomWalkResult res) } } + /// + /// Provide a Dictionary whose keys are "Ref" or equivalent + /// string values which link back to a "BomRef" hopefully + /// defined somewhere in the same Bom document (but may be + /// dangling, or sometimes co-opted with external links to + /// other Bom documents!), and whose values are lists of + /// BomEntities which use this same "ref" value. + /// + /// See also: GetBomRefsInContainers() with similar info + /// about keys which are BomEntity "containers" and values + /// are lists of BomEntity with a BomRef in those containers, + /// and GetBomRefsWithContainer() with transposed returns. + /// + /// + public Dictionary> GetRefsInContainers(BomWalkResult res) + { + AssertThisBomWalkResult(res); + return res.GetRefsInContainers(); + } + + /// + /// This is a run-once method to get a dictionary. + /// See GetRefsInContainers(BomWalkResult) for one using a cache + /// prepared by WalkThis() for mass manipulations. + /// + /// + public Dictionary> GetRefsInContainers() + { + BomWalkResult res = WalkThis(); + return GetRefsInContainers(res); + } + /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained From 590a619a4ced31e20ba5f6922064448b5f8ce265 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 13:02:13 +0200 Subject: [PATCH 275/285] Address Codacy complaints: redundant parentheses in new Dict<...>() {...} Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/EvidenceIdentity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/EvidenceIdentity.cs b/src/CycloneDX.Core/Models/EvidenceIdentity.cs index c76d4192..32529e32 100644 --- a/src/CycloneDX.Core/Models/EvidenceIdentity.cs +++ b/src/CycloneDX.Core/Models/EvidenceIdentity.cs @@ -72,7 +72,7 @@ public enum EvidenceFieldType public EvidenceTools Tools { get; set; } private static readonly ImmutableDictionary> RefLinkConstraints_List_AnyBomEntity = - new Dictionary>() + new Dictionary> { // EvidenceTools is a List as of CDX spec 1.5 { typeof(EvidenceIdentity).GetProperty("Tools", typeof(EvidenceTools)), RefLinkConstraints_AnyBomEntity } From 16e13003f04b3a07615914845f574c061e1315b1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 13:05:50 +0200 Subject: [PATCH 276/285] Address Codacy complaints: make use of referrerModified for return value, as planned - correct fix Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index a7607d04..d2b1ea61 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -481,6 +481,8 @@ public Dictionary GetBomRefsWithContainer() /// public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) { + bool somethingModified = false; + AssertThisBomWalkResult(res); if (oldRef is null || newRef is null || oldRef == newRef) { @@ -488,7 +490,7 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) // Note: not checking for xxxRef.Trim()=="" or trimmed-string // equalities as it is up to the caller how things were or // will be named. - return false; + return somethingModified; } if (newRef == "") @@ -890,11 +892,13 @@ public bool RenameBomRef(string oldRef, string newRef, BomWalkResult res) "a reference to a \"bom-ref\" identifier: " + oldRef); } } + + somethingModified |= (referrerModified == 0); } } // Survived without exceptions! ;) - return (referrerModified == 0); + return somethingModified; } /// From fe41274f73c9f1ce2068ca22344b22803af4f1bc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 13:08:22 +0200 Subject: [PATCH 277/285] Address Codacy complaints: twist back the needed assignment of debugPerformance even if privatized with getter/setter Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 5a6abfe5..13172443 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1370,7 +1370,7 @@ public class BomWalkResult // (and printing in ToString() method) to help // debug the data-walk overheads. Accounting // does have a cost (~5% for a larger 20s run). - private bool debugPerformance { get; set; } + private bool debugPerformance { get; set; } = false; // Helpers for performance accounting - how hard // was it to discover the information in this From e66bd4f23336b0a76847de68873e9ee139127296 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 13:11:02 +0200 Subject: [PATCH 278/285] Address Codacy complaints: be sure to not stop the stopWatchWalkTotal in (unlikely) case it is null Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 13172443..bd0fe057 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -2088,7 +2088,8 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) SerializeBomEntity_BomRefs((BomEntity)propVal, obj); } - if (isTimeAccounter && debugPerformance) + // nullness check seems bogus, but Codacy insists... + if (isTimeAccounter && debugPerformance && !(stopWatchWalkTotal is null)) { stopWatchWalkTotal.Stop(); } From 0b9fdb68b827ce7379913a77399d5ecb3113aa91 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 13:42:37 +0200 Subject: [PATCH 279/285] Address Codacy complaints: shut them off for debugPerformance being pre-initialized or not (and a definitive value wins over compiler default du-jour), because Sonar analyzer wants opposite things Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index bd0fe057..ce5e11db 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1370,7 +1370,9 @@ public class BomWalkResult // (and printing in ToString() method) to help // debug the data-walk overheads. Accounting // does have a cost (~5% for a larger 20s run). + #pragma warning disable S3052 private bool debugPerformance { get; set; } = false; + #pragma warning restore S3052 // Helpers for performance accounting - how hard // was it to discover the information in this From 2a2b7b51e32fa1972d67cfe5946f9e353f907a03 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 14:18:50 +0200 Subject: [PATCH 280/285] Address Codacy complaints fallout: add explicit getter/setter for BomWalkResult.bomRoot Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 4 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index d2b1ea61..103d6529 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -341,11 +341,11 @@ private void AssertThisBomWalkResult(BomWalkResult res) throw new ArgumentNullException("res"); } - if (!(Object.ReferenceEquals(res.bomRoot, this))) + if (!(Object.ReferenceEquals(res.GetBomRoot(), this))) { throw new BomEntityConflictException( "The specified BomWalkResult.bomRoot does not refer to this Bom document instance", - res.bomRoot.GetType()); + res.GetBomRoot().GetType()); } } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index ce5e11db..589371bc 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1342,7 +1342,7 @@ public class BomWalkResult /// The BomEntity (normally a whole Bom document) /// which was walked and reported here. /// - private BomEntity bomRoot { get; set; } + private BomEntity bomRoot; /// /// Populated by GetBomRefsInContainers(), @@ -2097,6 +2097,16 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) } } + public BomEntity GetBomRoot() + { + return bomRoot; + } + + public void SetBomRoot(BomEntity be) + { + bomRoot = be; + } + /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained From 86c38ce1ab79a1117a7ee371ef8af94921e8b8a4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 13 Oct 2023 14:19:23 +0200 Subject: [PATCH 281/285] Address Codacy complaints fallout: fix signature for Bom.GetRefsInContainers() wrap Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index 103d6529..ff39909e 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -363,7 +363,7 @@ private void AssertThisBomWalkResult(BomWalkResult res) /// and GetBomRefsWithContainer() with transposed returns. /// /// - public Dictionary> GetRefsInContainers(BomWalkResult res) + public Dictionary> GetRefsInContainers(BomWalkResult res) { AssertThisBomWalkResult(res); return res.GetRefsInContainers(); @@ -375,7 +375,7 @@ public Dictionary> GetRefsInContainers(BomWalkResult /// prepared by WalkThis() for mass manipulations. /// /// - public Dictionary> GetRefsInContainers() + public Dictionary> GetRefsInContainers() { BomWalkResult res = WalkThis(); return GetRefsInContainers(res); From 3c48440e23d0d74bb142c36968ced7e95686e27f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 14 Oct 2023 08:46:15 +0200 Subject: [PATCH 282/285] Address Codacy complaints fallout: *public*+get/set = love: fix back BomWalkResult.bomRoot Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Bom.cs | 4 ++-- src/CycloneDX.Core/Models/BomEntity.cs | 12 +----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/CycloneDX.Core/Models/Bom.cs b/src/CycloneDX.Core/Models/Bom.cs index ff39909e..3a2e17a0 100644 --- a/src/CycloneDX.Core/Models/Bom.cs +++ b/src/CycloneDX.Core/Models/Bom.cs @@ -341,11 +341,11 @@ private void AssertThisBomWalkResult(BomWalkResult res) throw new ArgumentNullException("res"); } - if (!(Object.ReferenceEquals(res.GetBomRoot(), this))) + if (!(Object.ReferenceEquals(res.bomRoot, this))) { throw new BomEntityConflictException( "The specified BomWalkResult.bomRoot does not refer to this Bom document instance", - res.GetBomRoot().GetType()); + res.bomRoot.GetType()); } } diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 589371bc..2cd0214d 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1342,7 +1342,7 @@ public class BomWalkResult /// The BomEntity (normally a whole Bom document) /// which was walked and reported here. /// - private BomEntity bomRoot; + public BomEntity bomRoot { get; private set; } /// /// Populated by GetBomRefsInContainers(), @@ -2097,16 +2097,6 @@ public void SerializeBomEntity_BomRefs(BomEntity obj, BomEntity container) } } - public BomEntity GetBomRoot() - { - return bomRoot; - } - - public void SetBomRoot(BomEntity be) - { - bomRoot = be; - } - /// /// Provide a Dictionary whose keys are container BomEntities /// and values are lists of one or more directly contained From 3d553e9cbf716935209001a199c845428f57f630 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 14 Oct 2023 09:15:31 +0200 Subject: [PATCH 283/285] Component.cs: update comment Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/Component.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index db73e1a9..5d679553 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -235,7 +235,7 @@ public bool Equivalent(Component obj) // for many computer-generated BOMs the "bom-ref" is representative. // Notably, we care about BomRef because we might refer to this // entity from others in the same document (e.g. Dependencies[]), - // and BOM references are done by this value. + // and (inter-)BOM references are done by this value. // NOTE: If two otherwise identical components are refferred to // by different BomRef values - so be it, Bom duplicates remain. if (this.Type == obj.Type // No nullness check here, or we get: error CS0037: Cannot convert null to 'Component.Classification' because it is a non-nullable value type From 5c1e10cbb55ca33b4a5e10cc9895185729d863a8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 14 Oct 2023 09:21:27 +0200 Subject: [PATCH 284/285] BomEntity.cs: BomWalkResult: update comment about performance-accounting info exposure Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 2cd0214d..38f98a6f 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1377,6 +1377,12 @@ public class BomWalkResult // Helpers for performance accounting - how hard // was it to discover the information in this // BomWalkResult object? + // TOTHINK: Expose these values directly, for + // very curious callers (for whom ToString() + // would not suffice)? + // * Use public getter/private setter? or... + // * Method to export a Dictionary of values, + // including a momentary reading of stopwatch? private int sbeCountMethodEnter { get; set; } private int sbeCountMethodQuickExit { get; set; } private int sbeCountPropInfoEnter { get; set; } From 2e52406d1389ed68abb5bb340d866bf0cc5fbaea Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 14 Oct 2023 09:21:47 +0200 Subject: [PATCH 285/285] Address Codacy complaints fallout: *public*+get/set = love: fix back BomWalkResult.debugPerformance Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Models/BomEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CycloneDX.Core/Models/BomEntity.cs b/src/CycloneDX.Core/Models/BomEntity.cs index 38f98a6f..40ffd385 100644 --- a/src/CycloneDX.Core/Models/BomEntity.cs +++ b/src/CycloneDX.Core/Models/BomEntity.cs @@ -1371,7 +1371,7 @@ public class BomWalkResult // debug the data-walk overheads. Accounting // does have a cost (~5% for a larger 20s run). #pragma warning disable S3052 - private bool debugPerformance { get; set; } = false; + public bool debugPerformance { get; set; } = false; #pragma warning restore S3052 // Helpers for performance accounting - how hard