Sha256 hash with spans (#107)

* Sha256 hash with spans

* HashData

* NumericFormat

* Tests

* md5 test

* xml

* use utf16 encoding rather than utf8

---------

Co-authored-by: Alan Rynne <alan@rynne.es>
This commit is contained in:
Jedd Morgan
2024-09-09 13:43:32 +01:00
committed by GitHub
parent 296a00e2eb
commit 450dbcce81
8 changed files with 198 additions and 67 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
IDE0044;IDE0130;CA1508;
<!-- Analysers that provide no tangeable value to a test project -->
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;
</NoWarn>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
</PropertyGroup>
+50 -12
View File
@@ -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
/// <param name="input">the value to hash</param>
/// <param name="format">NumericFormat</param>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
/// <param name="length">Desired length of the returned string. Must be 2 &#x2264; Length &#x2264; 64, and must be a multiple of 2</param>
/// <returns><inheritdoc cref="Sha256(string, string?, int)"/></returns>
[Pure]
public static string Sha256(
ReadOnlySpan<char> input,
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
int length = SHA256.HashSizeInBytes * sizeof(char)
)
{
ReadOnlySpan<byte> inputBytes = MemoryMarshal.AsBytes(input);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(inputBytes, hash);
Span<char> 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
/// <param name="input">the value to hash</param>
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
/// <param name="length">Desired length of the returned string</param>
/// <returns>the hash string</returns>
/// <exception cref="FormatException"><paramref name="format"/> is not a recognised numeric format</exception>
/// <exception cref="ArgumentOutOfRangeException"><inheritdoc cref="StringBuilder.ToString(int, int)"/></exception>
[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);
}
/// <inheritdoc cref="Sha256"/>
/// <inheritdoc cref="Sha256(string, string?, int)"/>
/// <remarks>MD5 is a broken cryptographic algorithm and should be used subject to review see CA5351</remarks>
[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);
}
}
@@ -465,7 +465,11 @@ public class SpeckleObjectSerializer
[Pure]
private static string ComputeId(IReadOnlyDictionary<string, object?> obj)
{
#if NET6_0_OR_GREATER
ReadOnlySpan<char> serialized = JsonConvert.SerializeObject(obj).AsSpan();
#else
string serialized = JsonConvert.SerializeObject(obj);
#endif
string hash = Crypt.Sha256(serialized, length: HashUtility.HASH_LENGTH);
return hash;
}
@@ -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());
}
}
@@ -27,10 +27,11 @@ public class GeneralDeserializer : IDisposable
}
[Benchmark]
public Base RunTest()
public async Task<Base> 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]
@@ -10,9 +10,10 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="Speckle.Objects" VersionOverride="3.1.0-dev.109"/> <!--Latest-->
<ProjectReference Include="..\..\src\Speckle.Objects\Speckle.Objects.csproj" />
<!-- <PackageReference Include="Speckle.Objects" VersionOverride="3.1.0-dev.109"/> &lt;!&ndash;Latest&ndash;&gt;-->
<!-- <PackageReference Include="Speckle.Objects" VersionOverride="3.0.0-dev.51"/> &lt;!&ndash;Pre-optimisation&ndash;&gt;-->
</ItemGroup>
</Project>
@@ -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, )",
@@ -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));
}
}