mirror of https://github.com/Squidex/squidex.git
Browse Source
* Improve performance. * Minor formatting. * Mini improvements. * Fix hasher.pull/1012/head
committed by
GitHub
27 changed files with 943 additions and 397 deletions
@ -0,0 +1,252 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Buffers; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
|
|||
namespace Squidex.Infrastructure; |
|||
|
|||
public static class HashExtensions |
|||
{ |
|||
private const int MaxStackSize = 128; |
|||
private delegate int HashMethod(ReadOnlySpan<byte> source, Span<byte> destination); |
|||
private delegate string EncodeMethod(ReadOnlySpan<byte> source); |
|||
private static readonly HashMethod HashMD5 = MD5.HashData; |
|||
private static readonly HashMethod HashSHA256 = SHA256.HashData; |
|||
private static readonly HashMethod HashSHA512 = SHA512.HashData; |
|||
private static readonly EncodeMethod EncodeUTF8 = Encoding.UTF8.GetString; |
|||
private static readonly EncodeMethod EncodeBase64 = input => Convert.ToBase64String(input, default); |
|||
|
|||
public static string ToSha256Base64(this string value) |
|||
{ |
|||
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, Encoding.UTF8, EncodeBase64); |
|||
} |
|||
|
|||
public static string ToSha256Base64(this ReadOnlySpan<byte> value) |
|||
{ |
|||
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, EncodeBase64); |
|||
} |
|||
|
|||
public static string ToSha512(this string value) |
|||
{ |
|||
return ToHashed(value, HashSHA512, SHA512.HashSizeInBits, Encoding.UTF8, EncodeUTF8); |
|||
} |
|||
|
|||
public static string ToSha512(this ReadOnlySpan<byte> value) |
|||
{ |
|||
return ToHashed(value, HashSHA512, SHA512.HashSizeInBits, EncodeUTF8); |
|||
} |
|||
|
|||
public static string ToSha256(this string value) |
|||
{ |
|||
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, Encoding.UTF8, EncodeUTF8); |
|||
} |
|||
|
|||
public static string ToSha256(this ReadOnlySpan<byte> value) |
|||
{ |
|||
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, EncodeUTF8); |
|||
} |
|||
|
|||
public static string ToMD5(this string value) |
|||
{ |
|||
return ToHashed(value, HashMD5, MD5.HashSizeInBits, Encoding.UTF8, EncodeUTF8); |
|||
} |
|||
|
|||
public static string ToMD5(this ReadOnlySpan<byte> value) |
|||
{ |
|||
return ToHashed(value, HashMD5, MD5.HashSizeInBits, EncodeUTF8); |
|||
} |
|||
|
|||
private static string ToHashed(this string value, HashMethod algorithm, int hashSize, Encoding encoding, EncodeMethod encode) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(value)) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
string result; |
|||
|
|||
var length = encoding.GetByteCount(value); |
|||
|
|||
if (length > MaxStackSize) |
|||
{ |
|||
var buffer = ArrayPool<byte>.Shared.Rent(length); |
|||
try |
|||
{ |
|||
result = ConvertCore(algorithm, hashSize, value.AsSpan(), buffer, encode, encoding); |
|||
} |
|||
finally |
|||
{ |
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Span<byte> buffer = stackalloc byte[length]; |
|||
|
|||
result = ConvertCore(algorithm, hashSize, value.AsSpan(), buffer, encode, encoding); |
|||
} |
|||
|
|||
static string ConvertCore(HashMethod algorithm, int hashSize, ReadOnlySpan<char> source, Span<byte> destination, EncodeMethod encode, Encoding encoding) |
|||
{ |
|||
var written = encoding.GetBytes(source, destination); |
|||
|
|||
return ToHashed(destination[..written], algorithm, hashSize, encode); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static string ToHashed(ReadOnlySpan<byte> bytes, HashMethod algorithm, int hashSize, EncodeMethod encode) |
|||
{ |
|||
if (bytes.Length == 0) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
string result; |
|||
|
|||
var length = hashSize / 8; |
|||
|
|||
if (length > MaxStackSize) |
|||
{ |
|||
var buffer = ArrayPool<byte>.Shared.Rent(length); |
|||
try |
|||
{ |
|||
result = ConvertCore(algorithm, bytes, buffer, encode); |
|||
} |
|||
finally |
|||
{ |
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Span<byte> buffer = stackalloc byte[length]; |
|||
|
|||
return ConvertCore(algorithm, bytes, buffer, encode); |
|||
} |
|||
|
|||
return result; |
|||
|
|||
static string ConvertCore(HashMethod algorithm, ReadOnlySpan<byte> source, Span<byte> destination, EncodeMethod encode) |
|||
{ |
|||
var written = algorithm(source, destination); |
|||
|
|||
return encode(destination[..written]); |
|||
} |
|||
} |
|||
|
|||
public static void AppendString(this IncrementalHash hash, string value) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(value)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var length = Encoding.Default.GetByteCount(value); |
|||
|
|||
if (length > MaxStackSize) |
|||
{ |
|||
var buffer = ArrayPool<byte>.Shared.Rent(length); |
|||
try |
|||
{ |
|||
ConvertCore(hash, value, buffer); |
|||
} |
|||
finally |
|||
{ |
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Span<byte> buffer = stackalloc byte[length]; |
|||
|
|||
ConvertCore(hash, value, buffer); |
|||
} |
|||
|
|||
static void ConvertCore(IncrementalHash hash, string source, Span<byte> destination) |
|||
{ |
|||
var written = Encoding.Default.GetBytes(source, destination); |
|||
|
|||
hash.AppendData(destination[..written]); |
|||
} |
|||
} |
|||
|
|||
public static void AppendLong(this IncrementalHash hash, long version) |
|||
{ |
|||
if (version == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Span<byte> buffer = stackalloc byte[sizeof(long)]; |
|||
|
|||
if (BitConverter.TryWriteBytes(buffer, version)) |
|||
{ |
|||
hash.AppendData(buffer); |
|||
} |
|||
} |
|||
|
|||
public static string GetHexStringAndReset(this IncrementalHash hash) |
|||
{ |
|||
Span<byte> buffer = stackalloc byte[hash.HashLengthInBytes]; |
|||
|
|||
hash.GetHashAndReset(buffer); |
|||
|
|||
return ToHexString(buffer); |
|||
} |
|||
|
|||
public static string ToHexString(this ReadOnlySpan<byte> data) |
|||
{ |
|||
string result; |
|||
|
|||
var length = data.Length * 2; |
|||
|
|||
if (length > MaxStackSize) |
|||
{ |
|||
var buffer = ArrayPool<char>.Shared.Rent(length); |
|||
try |
|||
{ |
|||
result = ConvertCore(data, buffer); |
|||
} |
|||
finally |
|||
{ |
|||
ArrayPool<char>.Shared.Return(buffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Span<char> buffer = stackalloc char[length]; |
|||
|
|||
result = ConvertCore(data, buffer); |
|||
} |
|||
|
|||
return result; |
|||
|
|||
static string ConvertCore(ReadOnlySpan<byte> data, Span<char> hexBuffer) |
|||
{ |
|||
unchecked |
|||
{ |
|||
int n = 0; |
|||
for (int i = 0; i < data.Length; i++) |
|||
{ |
|||
byte b = data[i]; |
|||
|
|||
byte b1 = (byte)(b >> 4); |
|||
byte b2 = (byte)(b & 0xF); |
|||
|
|||
hexBuffer[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10)); |
|||
hexBuffer[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10)); |
|||
} |
|||
} |
|||
|
|||
return new string(hexBuffer); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,182 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
|
|||
namespace Squidex.Infrastructure; |
|||
|
|||
public class HashExtensionsTests |
|||
{ |
|||
public static IEnumerable<object[]> InputLengths() |
|||
{ |
|||
yield return new object[] { 1 }; |
|||
yield return new object[] { 20 }; |
|||
yield return new object[] { 50 }; |
|||
yield return new object[] { 500 }; |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_calculate_hex_code_from_empty_array() |
|||
{ |
|||
var source = Array.Empty<byte>(); |
|||
|
|||
var actual = ((ReadOnlySpan<byte>)source.AsSpan()).ToHexString(); |
|||
|
|||
Assert.Equal(string.Empty, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_calculate_hex_code_from_byte_array() |
|||
{ |
|||
var source = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C }; |
|||
|
|||
var actual = ((ReadOnlySpan<byte>)source.AsSpan()).ToHexString(); |
|||
|
|||
Assert.Equal("0001FF1A2B3C", actual); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_md5(int length) |
|||
{ |
|||
AssertHash(HashExtensions.ToMD5, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_md5_from_bytes(int length) |
|||
{ |
|||
AssertHashFromBytes(HashExtensions.ToMD5, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_sha256(int length) |
|||
{ |
|||
AssertHash(HashExtensions.ToSha256, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_sha256_from_bytes(int length) |
|||
{ |
|||
AssertHashFromBytes(HashExtensions.ToSha256, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_sha512(int length) |
|||
{ |
|||
AssertHash(HashExtensions.ToSha512, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_sha512_from_bytes(int length) |
|||
{ |
|||
AssertHashFromBytes(HashExtensions.ToSha512, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_to_sha256_base64(int length) |
|||
{ |
|||
AssertHash(HashExtensions.ToSha256Base64, length); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(InputLengths))] |
|||
public void Should_hash_sha256_base64_from_bytes(int length) |
|||
{ |
|||
AssertHashFromBytes(HashExtensions.ToSha256Base64, length); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_convert_same_hash_as_webhook_utils() |
|||
{ |
|||
var input = $"{Guid.NewGuid()}{new string('0', 100)}{Guid.NewGuid()}"; |
|||
|
|||
var hash1 = WebhookHash(input); |
|||
var hash2 = input.ToSha256Base64(); |
|||
|
|||
Assert.Equal(hash1, hash2); |
|||
} |
|||
|
|||
private delegate string HashFromBytes(ReadOnlySpan<byte> source); |
|||
|
|||
private static void AssertHashFromBytes(HashFromBytes hasher, int length) |
|||
{ |
|||
var input1 = CreateBuffer(length, 0); |
|||
var input2 = CreateBuffer(length, 42); |
|||
|
|||
var hash1_a = hasher(input1); |
|||
var hash1_b = hasher(input1); |
|||
|
|||
var hash2 = hasher(input2); |
|||
|
|||
Assert.NotNull(hash1_a); |
|||
Assert.NotNull(hash1_a); |
|||
Assert.NotNull(hash2); |
|||
|
|||
Assert.NotEmpty(hash1_a); |
|||
Assert.NotEmpty(hash1_b); |
|||
Assert.NotEmpty(hash2); |
|||
|
|||
Assert.Equal(hash1_a, hash1_b); |
|||
|
|||
Assert.NotEqual(hash1_a, hash2); |
|||
} |
|||
|
|||
private static void AssertHash(Func<string, string> hasher, int length) |
|||
{ |
|||
var input1 = new string('a', length); |
|||
var input2 = new string('b', length); |
|||
|
|||
var hash1_a = hasher(input1); |
|||
var hash1_b = hasher(input1); |
|||
|
|||
var hash2 = hasher(input2); |
|||
|
|||
Assert.NotNull(hash1_a); |
|||
Assert.NotNull(hash1_a); |
|||
Assert.NotNull(hash2); |
|||
|
|||
Assert.NotEmpty(hash1_a); |
|||
Assert.NotEmpty(hash1_b); |
|||
Assert.NotEmpty(hash2); |
|||
|
|||
Assert.Equal(hash1_a, hash1_b); |
|||
|
|||
Assert.NotEqual(hash1_a, hash2); |
|||
} |
|||
|
|||
private static byte[] CreateBuffer(int length, int offset) |
|||
{ |
|||
var input = new byte[length]; |
|||
|
|||
for (var i = 0; i < length; i++) |
|||
{ |
|||
input[i] = (byte)(i + offset); |
|||
} |
|||
|
|||
return input; |
|||
} |
|||
|
|||
private static string WebhookHash(string value) |
|||
{ |
|||
using (var sha = SHA256.Create()) |
|||
{ |
|||
var bytesArray = Encoding.UTF8.GetBytes(value); |
|||
var bytesHash = sha.ComputeHash(bytesArray); |
|||
|
|||
var result = Convert.ToBase64String(bytesHash); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -1,98 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Reflection.Internal; |
|||
|
|||
namespace Squidex.Infrastructure.Reflection; |
|||
|
|||
public class PropertiesTypeAccessorTests |
|||
{ |
|||
public class TestClass |
|||
{ |
|||
private int target; |
|||
|
|||
public int ReadWrite |
|||
{ |
|||
get |
|||
{ |
|||
return target; |
|||
} |
|||
set |
|||
{ |
|||
target = value; |
|||
} |
|||
} |
|||
|
|||
public int Read |
|||
{ |
|||
get => target; |
|||
} |
|||
|
|||
public int Write |
|||
{ |
|||
set => target = value; |
|||
} |
|||
} |
|||
|
|||
private readonly TestClass target = new TestClass(); |
|||
|
|||
[Fact] |
|||
public void Should_set_read_write_property() |
|||
{ |
|||
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); |
|||
|
|||
sut.Set(target, 123); |
|||
|
|||
Assert.Equal(123, target.Read); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_write_property() |
|||
{ |
|||
var accessor = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); |
|||
|
|||
accessor.Set(target, 123); |
|||
|
|||
Assert.Equal(123, target.Read); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_if_setting_readonly() |
|||
{ |
|||
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => sut.Set(target, 123)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_read_write_property() |
|||
{ |
|||
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); |
|||
|
|||
target.Write = 123; |
|||
|
|||
Assert.Equal(123, sut.Get(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_read_property() |
|||
{ |
|||
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); |
|||
|
|||
target.Write = 123; |
|||
|
|||
Assert.Equal(123, sut.Get(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_if_getting_writeonly_property() |
|||
{ |
|||
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => sut.Get(target)); |
|||
} |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Reflection.Internal; |
|||
|
|||
namespace Squidex.Infrastructure.Reflection; |
|||
|
|||
public class PropertyAccessorTests |
|||
{ |
|||
public class TestClass |
|||
{ |
|||
private int readWrite; |
|||
private int readWriteVirtual; |
|||
|
|||
public int ReadWrite |
|||
{ |
|||
get |
|||
{ |
|||
return readWrite; |
|||
} |
|||
set |
|||
{ |
|||
readWrite = value; |
|||
} |
|||
} |
|||
|
|||
public virtual int ReadWriteVirtual |
|||
{ |
|||
get |
|||
{ |
|||
return readWriteVirtual; |
|||
} |
|||
set |
|||
{ |
|||
readWriteVirtual = value; |
|||
} |
|||
} |
|||
|
|||
public int ReadWriteAuto { get; set; } |
|||
|
|||
public int Read |
|||
{ |
|||
get => readWrite; |
|||
} |
|||
|
|||
public int Write |
|||
{ |
|||
set => readWrite = value; |
|||
} |
|||
} |
|||
|
|||
private readonly TestClass target = new TestClass(); |
|||
|
|||
[Fact] |
|||
public void Should_set_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWrite")!); |
|||
|
|||
sut(target, 123); |
|||
|
|||
Assert.Equal(123, target.Read); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_virtual_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteVirtual")!); |
|||
|
|||
sut(target, 123); |
|||
|
|||
Assert.Equal(123, target.ReadWriteVirtual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_auto_implemented_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteAuto")!); |
|||
|
|||
sut(target, 123); |
|||
|
|||
Assert.Equal(123, target.ReadWriteAuto); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("Write")!); |
|||
|
|||
sut(target, 123); |
|||
|
|||
Assert.Equal(123, target.Read); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_if_setting_readonly() |
|||
{ |
|||
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("Read")!); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => sut(target, 123)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWrite")!); |
|||
|
|||
target.Write = 123; |
|||
|
|||
Assert.Equal(123, sut(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_virtual_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteVirtual")!); |
|||
|
|||
target.ReadWriteVirtual = 123; |
|||
|
|||
Assert.Equal(123, sut(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_auto_implemented_read_write_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteAuto")!); |
|||
|
|||
target.ReadWriteAuto = 123; |
|||
|
|||
Assert.Equal(123, sut(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_get_read_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("Read")!); |
|||
|
|||
target.Write = 123; |
|||
|
|||
Assert.Equal(123, sut(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_throw_exception_if_getting_writeonly_property() |
|||
{ |
|||
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("Write")!); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => sut(target)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue