diff --git a/Directory.Build.targets b/Directory.Build.targets index 4e66cb1e..8b4299de 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -7,7 +7,7 @@ CS0618;CA1034;CA2201;CA1051;CA1040;CA1724; IDE0044;IDE0130;CA1508; - CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019; + CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831; false diff --git a/src/Speckle.Sdk/Helpers/Crypt.cs b/src/Speckle.Sdk/Helpers/Crypt.cs index 835386f6..66aaa7c8 100644 --- a/src/Speckle.Sdk/Helpers/Crypt.cs +++ b/src/Speckle.Sdk/Helpers/Crypt.cs @@ -2,27 +2,61 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Security.Cryptography; using System.Text; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif namespace Speckle.Sdk.Helpers; public static class Crypt { +#if NET6_0_OR_GREATER /// the value to hash - /// NumericFormat - /// - /// + /// "x2" for lower case, "X2" for uppercase. + /// Desired length of the returned string. Must be 2 ≤ Length ≤ 64, and must be a multiple of 2 + /// + [Pure] + public static string Sha256( + ReadOnlySpan input, + [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", + int length = SHA256.HashSizeInBytes * sizeof(char) + ) + { + ReadOnlySpan inputBytes = MemoryMarshal.AsBytes(input); + + Span hash = stackalloc byte[SHA256.HashSizeInBytes]; + SHA256.HashData(inputBytes, hash); + + Span output = stackalloc char[length]; + + for (int i = 0, j = 0; j < length; i += sizeof(byte), j += sizeof(char)) + { + hash[i].TryFormat(output[j..], out _, format); + } + + return new string(output); + } +#endif + + /// the value to hash + /// "x2" for lower case, "X2" for uppercase. + /// Desired length of the returned string /// the hash string /// is not a recognised numeric format /// [Pure] - public static string Sha256(string input, string? format = "x2", int startIndex = 0, int length = 64) + public static string Sha256( + string input, + [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", + int length = 64 + ) { - var inputBytes = Encoding.UTF8.GetBytes(input); -#if NETSTANDARD2_0 + var inputBytes = Encoding.Unicode.GetBytes(input); +#if NET6_0_OR_GREATER + byte[] hash = SHA256.HashData(inputBytes); +#else using var sha256 = SHA256.Create(); byte[] hash = sha256.ComputeHash(inputBytes); -#else - byte[] hash = SHA256.HashData(inputBytes); #endif StringBuilder sb = new(64); @@ -31,14 +65,18 @@ public static class Crypt sb.Append(b.ToString(format)); } - return sb.ToString(startIndex, length); + return sb.ToString(0, length); } - /// + /// /// MD5 is a broken cryptographic algorithm and should be used subject to review see CA5351 [Pure] [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] - public static string Md5(string input, string? format = "x2", int startIndex = 0, int length = 32) + public static string Md5( + string input, + [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", + int length = 32 + ) { byte[] inputBytes = Encoding.ASCII.GetBytes(input.ToLowerInvariant()); #if NETSTANDARD2_0 @@ -53,6 +91,6 @@ public static class Crypt sb.Append(hashBytes[i].ToString(format)); } - return sb.ToString(startIndex, length); + return sb.ToString(0, length); } } diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs index 02a35726..86d33bc3 100644 --- a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs @@ -465,7 +465,11 @@ public class SpeckleObjectSerializer [Pure] private static string ComputeId(IReadOnlyDictionary obj) { +#if NET6_0_OR_GREATER + ReadOnlySpan serialized = JsonConvert.SerializeObject(obj).AsSpan(); +#else string serialized = JsonConvert.SerializeObject(obj); +#endif string hash = Crypt.Sha256(serialized, length: HashUtility.HASH_LENGTH); return hash; } diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs new file mode 100644 index 00000000..39889f50 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs @@ -0,0 +1,31 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using Speckle.Sdk.Helpers; + +namespace Speckle.Sdk.Tests.Performance.Benchmarks; + +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput)] +public class CryptSha256Hash +{ + private string testData; + + [GlobalSetup] + public void Setup() + { + var random = new Random(420); + testData = new string(Enumerable.Range(0, 10_000_000).Select(_ => (char)random.Next(32, 127)).ToArray()); + } + + [Benchmark] + public string Sha256() + { + return Crypt.Sha256(testData); + } + + [Benchmark] + public string Sha256_Span() + { + return Crypt.Sha256(testData.AsSpan()); + } +} diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs index b11a522c..d9cd2a4b 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs @@ -27,10 +27,11 @@ public class GeneralDeserializer : IDisposable } [Benchmark] - public Base RunTest() + public async Task RunTest() { - BaseObjectDeserializerV2 sut = new() { ReadTransport = _dataSource.Transport }; - return sut.Deserialize(_dataSource.Transport.GetObject(_dataSource.ObjectId)!); + SpeckleObjectDeserializer sut = new() { ReadTransport = _dataSource.Transport }; + string data = await _dataSource.Transport.GetObject(_dataSource.ObjectId)!; + return await sut.DeserializeJsonAsync(data); } [GlobalCleanup] diff --git a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj index 75c949bc..1fa88423 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj +++ b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj @@ -10,9 +10,10 @@ - + + + - diff --git a/tests/Speckle.Sdk.Tests.Performance/packages.lock.json b/tests/Speckle.Sdk.Tests.Performance/packages.lock.json index 63732fb5..c383e921 100644 --- a/tests/Speckle.Sdk.Tests.Performance/packages.lock.json +++ b/tests/Speckle.Sdk.Tests.Performance/packages.lock.json @@ -48,15 +48,6 @@ "resolved": "0.9.6", "contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w==" }, - "Speckle.Objects": { - "type": "Direct", - "requested": "[3.1.0-dev.109, )", - "resolved": "3.1.0-dev.109", - "contentHash": "RJNSq9GgPYrf3SZqnBSLxn7uwYSH7Df2WvKq1Q2itrPw+tBMHQ3mco1KpM0IyTYXEMc42qjeT/3JL9R3ZJFSvg==", - "dependencies": { - "Speckle.Sdk": "3.1.0-dev.109" - } - }, "BenchmarkDotNet.Annotations": { "type": "Transitive", "resolved": "0.14.0", @@ -263,28 +254,6 @@ "resolved": "0.3.17", "contentHash": "FQgtCoF2HFwvzKWulAwBS5BGLlh8pgbrJtOp47jyBwh2CW16juVtacN1azOA2BqdrJXkXTNLNRMo7ZlHHiuAnA==" }, - "Speckle.Sdk": { - "type": "Transitive", - "resolved": "3.1.0-dev.109", - "contentHash": "WXbMEyzdf2KVGC5gU5BSYHxBFFpAoDD7+qJ6L9XDNW01XoM+iB0jKmIxC3+qZwMZwdw0epLr5fXwIkVbCwyBPQ==", - "dependencies": { - "GraphQL.Client": "6.0.0", - "Microsoft.CSharp": "4.7.0", - "Microsoft.Data.Sqlite": "7.0.7", - "Polly": "7.2.3", - "Polly.Contrib.WaitAndRetry": "1.1.1", - "Polly.Extensions.Http": "3.0.0", - "Speckle.DoubleNumerics": "4.0.1", - "Speckle.Newtonsoft.Json": "13.0.2", - "Speckle.Sdk.Logging": "3.1.0-dev.109", - "System.Text.Json": "5.0.2" - } - }, - "Speckle.Sdk.Logging": { - "type": "Transitive", - "resolved": "3.1.0-dev.109", - "contentHash": "4j+Bis84dCc0YoY5B5PCBq2TEMEgfTr6inbrImTmupYw1cw2k3nNcG/60h18ia0QdeF/J0Lig9gbDjK8hHXBgg==" - }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.4", @@ -383,6 +352,30 @@ "resolved": "4.5.4", "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" }, + "speckle.objects": { + "type": "Project", + "dependencies": { + "Speckle.Sdk": "[1.0.0, )" + } + }, + "speckle.sdk": { + "type": "Project", + "dependencies": { + "GraphQL.Client": "[6.0.0, )", + "Microsoft.CSharp": "[4.7.0, )", + "Microsoft.Data.Sqlite": "[7.0.7, )", + "Polly": "[7.2.3, )", + "Polly.Contrib.WaitAndRetry": "[1.1.1, )", + "Polly.Extensions.Http": "[3.0.0, )", + "Speckle.DoubleNumerics": "[4.0.1, )", + "Speckle.Newtonsoft.Json": "[13.0.2, )", + "Speckle.Sdk.Logging": "[1.0.0, )", + "System.Text.Json": "[5.0.2, )" + } + }, + "speckle.sdk.logging": { + "type": "Project" + }, "GraphQL.Client": { "type": "CentralTransitive", "requested": "[6.0.0, )", diff --git a/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs b/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs index 9ff1fffd..0f1a10e9 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs @@ -1,32 +1,95 @@ using NUnit.Framework; using Speckle.Sdk.Helpers; -using Speckle.Sdk.Models; namespace Speckle.Sdk.Tests.Unit.Models; [TestFixture(TestOf = typeof(Crypt))] public sealed class HashUtilityTests { - [Test] - [TestOf(nameof(Crypt.Md5))] - [TestCase("WnAbz1hCznVmDh1", "ad48ff1e60ea2369de178aaab2fa99af")] - [TestCase("wQKrSUzBB7FI1o6", "2424cff4a88055b149e5ff2aaf0b3131")] - public void Md5(string input, string expected) + public static IEnumerable<(string input, string sha256, string md5)> SmallTestCases() { - var lower = Crypt.Md5(input, "x2"); - var upper = Crypt.Md5(input, "X2"); - Assert.That(lower, Is.EqualTo(expected.ToLower())); - Assert.That(upper, Is.EqualTo(expected.ToUpper())); + yield return ( + "fxFB14cBcXvoENN", + "491267c87e343c2a4f9070034f4f8966e8ee4c14e5baf6f49289833142e5b509", + "d38572fdb20fe90c4871178df3f9570d" + ); + yield return ( + "tgWsOH8frdAwJT7", + "dd62d2028d8243f07cbdbb0cd4c3460a96c88dd6322dd9fceba4e4912ad88fa7", + "a7eecf20d68f836f462963928cd0f1a1" + ); + yield return ( + "wQKrSUzBB7FI1o6", + "70be5055f737e05d287c8898c7fcd3342733a337b67fe64f91fd34dcdf92fc88", + "2424cff4a88055b149e5ff2aaf0b3131" + ); + yield return ( + "WnAbz1hCznVmDh1", + "511433f4bb8d24d4ef7d4478984fd36f17ab6c58676f40ad0f4bcb615de0e313", + "ad48ff1e60ea2369de178aaab2fa99af" + ); } - [TestCase("fxFB14cBcXvoENN", "887db9349afa455f957a95f9dbacbb3c10697749cf4d4afc5c6398932a596fbc")] - [TestCase("tgWsOH8frdAwJT7", "e486224ded0dcb1452d69d0d005a6dcbc52087f6e8c66e04803e1337a192abb4")] - [TestOf(nameof(Crypt.Sha256))] - public void Sha256(string input, string expected) + public static IEnumerable<(string input, string sha256, string md5)> LargeTestCases() { - var lower = Crypt.Sha256(input, "x2"); - var upper = Crypt.Sha256(input, "X2"); - Assert.That(lower, Is.EqualTo(expected.ToLower())); - Assert.That(upper, Is.EqualTo(expected.ToUpper())); + Random random = new(420); + yield return ( + new string(Enumerable.Range(0, 1_000_000).Select(_ => (char)random.Next(32, 127)).ToArray()), + "b919b9e60cd6bb86ab395ee1408e12efd4d3e4e7b58f02b4cda6b4120086959a", + "d38572fdb20fe90c4871178df3f9570d" + ); + yield return ( + new string(Enumerable.Range(0, 10_000_000).Select(_ => (char)random.Next(32, 127)).ToArray()), + "f2e83101c3066c8a2983acdb92df53504ec00ac1e5afb71b7c3798cb4daf6162", + "a7eecf20d68f836f462963928cd0f1a1" + ); + } + + [Test, TestOf(nameof(Crypt.Md5))] + public void Md5( + [ValueSource(nameof(SmallTestCases))] (string input, string _, string expected) testCase, + [Range(0, 32)] int length + ) + { + var lower = Crypt.Md5(testCase.input, "x2", length); + var upper = Crypt.Md5(testCase.input, "X2", length); + + Assert.That(lower, Is.EqualTo(new string(testCase.expected.ToLower()[..length]))); + Assert.That(upper, Is.EqualTo(new string(testCase.expected.ToUpper()[..length]))); + } + + [Test, TestOf(nameof(Crypt.Sha256))] + public void Sha256( + [ValueSource(nameof(SmallTestCases))] (string input, string expected, string _) testCase, + [Range(2, 64)] int length + ) + { + var lower = Crypt.Sha256(testCase.input, "x2", length); + var upper = Crypt.Sha256(testCase.input, "X2", length); + + Assert.That(lower, Is.EqualTo(new string(testCase.expected.ToLower()[..length]))); + Assert.That(upper, Is.EqualTo(new string(testCase.expected.ToUpper()[..length]))); + } + + [Test, TestOf(nameof(Crypt.Sha256))] + public void Sha256_Span( + [ValueSource(nameof(SmallTestCases))] (string input, string expected, string _) testCase, + [Range(2, 64, 2)] int length //Span version of the function must have multiple of 2 + ) + { + var lower64 = Crypt.Sha256(testCase.input.AsSpan(), "x2", length); + var upper64 = Crypt.Sha256(testCase.input.AsSpan(), "X2", length); + + Assert.That(lower64, Is.EqualTo(new string(testCase.expected.ToLower()[..length]))); + Assert.That(upper64, Is.EqualTo(new string(testCase.expected.ToUpper()[..length]))); + } + + [Test, TestOf(nameof(Crypt.Sha256))] + [TestCaseSource(nameof(LargeTestCases))] + public void Sha256_LargeDataTests((string input, string expected, string _) testCase) + { + var test = Crypt.Sha256(testCase.input.AsSpan()); + + Assert.That(test, Is.EqualTo(testCase.expected)); } }