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:
@@ -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>
|
||||
|
||||
@@ -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 ≤ Length ≤ 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"/> <!–Latest–>-->
|
||||
<!-- <PackageReference Include="Speckle.Objects" VersionOverride="3.0.0-dev.51"/> <!–Pre-optimisation–>-->
|
||||
</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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user