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));
}
}