diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/Friends.cs b/src/Microsoft.TestPlatform.AdapterUtilities/Friends.cs new file mode 100644 index 0000000000..17825fd742 --- /dev/null +++ b/src/Microsoft.TestPlatform.AdapterUtilities/Friends.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.TestPlatform.AdapterUtilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/HierarchyConstants.cs b/src/Microsoft.TestPlatform.AdapterUtilities/HierarchyConstants.cs index 71eaba0d9d..1546637c7c 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/HierarchyConstants.cs +++ b/src/Microsoft.TestPlatform.AdapterUtilities/HierarchyConstants.cs @@ -26,7 +26,7 @@ public static class Levels /// /// Total length of Hierarchy array. /// - public const int TotalLevelCount = 4; + public const int TotalLevelCount = 2; /// /// Index of the namespace element of the array. @@ -37,16 +37,6 @@ public static class Levels /// Index of the class element of the array. /// public const int ClassIndex = 1; - - /// - /// Index of the test group element of the array. - /// - public const int TestGroupIndex = 2; - - /// - /// Index of the display name element of the array. - /// - public const int DisplayNameIndex = 3; } } } diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameHelper.Reflection.cs b/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameHelper.Reflection.cs index cfd2b08f4c..d7dc917413 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameHelper.Reflection.cs +++ b/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameHelper.Reflection.cs @@ -47,6 +47,42 @@ public static partial class ManagedNameHelper /// the RFC. /// public static void GetManagedName(MethodBase method, out string managedTypeName, out string managedMethodName) + => GetManagedName(method, out managedTypeName, out managedMethodName, out _); + + /// + /// Gets fully qualified managed type and method name from given instance. + /// + /// + /// A instance to get fully qualified managed type and method name. + /// + /// + /// When this method returns, contains the fully qualified managed type name of the . + /// This parameter is passed uninitialized; any value originally supplied in result will be overwritten. + /// The format is defined in the RFC. + /// + /// + /// When this method returns, contains the fully qualified managed method name of the . + /// This parameter is passed uninitialized; any value originally supplied in result will be overwritten. + /// The format is defined in the RFC. + /// + /// + /// When this method returns, contains the default test hierarchy values of the . + /// This parameter is passed uninitialized; any value originally supplied in result will be overwritten. + /// + /// + /// is null. + /// + /// + /// must describe a method. + /// + /// + /// Required functionality on is missing on the current platform. + /// + /// + /// More information about and can be found in + /// the RFC. + /// + public static void GetManagedName(MethodBase method, out string managedTypeName, out string managedMethodName, out string[] hierarchyValues) { if (method == null) { @@ -84,7 +120,7 @@ public static void GetManagedName(MethodBase method, out string managedTypeName, var methodBuilder = new StringBuilder(); // Namespace and Type Name (with arity designation) - AppendTypeString(typeBuilder, semanticType, closedType: false); + var hierarchyPos = AppendTypeString(typeBuilder, semanticType, closedType: false); // Method Name with method arity var arity = method.GetGenericArguments().Length; @@ -111,6 +147,10 @@ public static void GetManagedName(MethodBase method, out string managedTypeName, managedTypeName = typeBuilder.ToString(); managedMethodName = methodBuilder.ToString(); + hierarchyValues = new[] { + managedTypeName.Substring(hierarchyPos[0], hierarchyPos[1] - hierarchyPos[0]), + managedTypeName.Substring(hierarchyPos[1] + 1, hierarchyPos[2] - hierarchyPos[1] - 1), + }; } /// @@ -233,11 +273,13 @@ bool filter(MemberInfo mbr, object param) #endif } - private static void AppendTypeString(StringBuilder b, Type type, bool closedType) + private static int[] AppendTypeString(StringBuilder b, Type type, bool closedType) { + int[] hierarchies = null; + if (type.IsArray) { - AppendTypeString(b, type.GetElementType(), closedType); + hierarchies = AppendTypeString(b, type.GetElementType(), closedType); b.Append('['); for (int i = 0; i < type.GetArrayRank() - 1; i++) { @@ -256,16 +298,23 @@ private static void AppendTypeString(StringBuilder b, Type type, bool closedType } else { + hierarchies = new int[3]; + hierarchies[0] = b.Length; + AppendNamespace(b, type.Namespace); + hierarchies[1] = b.Length; + b.Append('.'); AppendNestedTypeName(b, type); - if (closedType) { AppendGenericTypeParameters(b, type); } + hierarchies[2] = b.Length; } + + return hierarchies; } private static void AppendNamespace(StringBuilder b, string namespaceString) diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs new file mode 100644 index 0000000000..5decc3eaf0 --- /dev/null +++ b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.TestPlatform.AdapterUtilities +{ + using System; + using System.Text; + + public class TestIdProvider + { + internal const int BlockBits = 512; + internal const int DigestBits = 160; + internal const int BlockBytes = BlockBits / 8; + internal const int DigestBytes = DigestBits / 8; + + private Guid id = Guid.Empty; + private byte[] hash = null; + private byte[] lastBlock = new byte[BlockBytes]; + private int position = 0; + + private readonly Sha1Implementation hasher; + + public TestIdProvider() + { + hasher = new Sha1Implementation(); + } + + public void AppendString(string str) + { + if (hash != null) + { + throw new InvalidOperationException(); + } + + var bytes = Encoding.Unicode.GetBytes(str); + var end = Math.Min(BlockBytes - position, bytes.Length); + + Buffer.BlockCopy(bytes, 0, lastBlock, position, end); + + // Block length is not reached yet. + if (end + position < BlockBytes) + { + position += end; + return; + } + + hasher.ProcessBlock(lastBlock, 0, lastBlock.Length); + position = 0; + + // We proccessed the entire string already + if (end == bytes.Length) + { + return; + } + + int start = 0; + while (end < bytes.Length) + { + start = end; + end += BlockBytes; + if (end > bytes.Length) + { + break; + } + + hasher.ProcessBlock(bytes, start, end - start); + } + + if (end > bytes.Length) + { + position = bytes.Length - start; + Buffer.BlockCopy(bytes, start, lastBlock, 0, position); + } + } + + public byte[] GetHash() + { + if (hash != null) + { + return hash; + } + + if (position != 0) + { + hasher.PadMessage(ref lastBlock, position); + hasher.ProcessBlock(lastBlock, 0, lastBlock.Length); + } + + hash = hasher.ProcessFinalBlock(); + + return hash; + } + + public Guid GetId() + { + if (id != Guid.Empty) + { + return id; + } + + var toGuid = new byte[16]; + Array.Copy(GetHash(), toGuid, 16); + id = new Guid(toGuid); + + return id; + } + + /// + /// SHA-1 Implementation as in https://tools.ietf.org/html/rfc3174 + /// + /// + /// This implementation only works with messages with a length + /// that is a multiple of the size of 8-bits. + /// + internal class Sha1Implementation + { + /* + * Many of the variable, function and parameter names in this code + * were used because those were the names used in the publication. + * + * For more information please refer to https://tools.ietf.org/html/rfc3174. + */ + + private int streamSize = 0; + private bool messagePadded = false; + + public Sha1Implementation() + { + Reset(); + } + + /// + /// A sequence of logical functions to be used in SHA-1. + /// Each f(t), 0 <= t <= 79, operates on three 32-bit words B, C, D and produces a 32-bit word as output. + /// + /// Function index. 0 <= t <= 79 + /// Word B + /// Word C + /// Word D + /// + /// f(t;B,C,D) = (B AND C) OR ((NOT B) AND D) ( 0 <= t <= 19) + /// f(t;B,C,D) = B XOR C XOR D (20 <= t <= 39) + /// f(t;B,C,D) = (B AND C) OR (B AND D) OR (C AND D) (40 <= t <= 59) + /// f(t;B,C,D) = B XOR C XOR D (60 <= t <= 79) + /// + private static uint F(int t, uint B, uint C, uint D) + { + if (t >= 0 && t <= 19) + { + return (B & C) | (~B & D); + } + else if ((t >= 20 && t <= 39) || (t >= 60 && t <= 79)) + { + return B ^ C ^ D; + } + else if (t >= 40 && t <= 59) + { + return (B & C) | (B & D) | (C & D); + } + else + { + throw new ArgumentException("Argument out of bounds! 0 <= t < 80", nameof(t)); + } + } + + /// + /// Returns a constant word K(t) which is used in the SHA-1. + /// + /// Word index. + /// + /// K(t) = 0x5A827999 ( 0 <= t <= 19) + /// K(t) = 0x6ED9EBA1 (20 <= t <= 39) + /// K(t) = 0x8F1BBCDC (40 <= t <= 59) + /// K(t) = 0xCA62C1D6 (60 <= t <= 79) + /// + private static uint K(int t) + { + if (t >= 0 && t <= 19) + { + return 0x5A827999u; + } + else if (t >= 20 && t <= 39) + { + return 0x6ED9EBA1u; + } + else if (t >= 40 && t <= 59) + { + return 0x8F1BBCDCu; + } + else if (t >= 60 && t <= 79) + { + return 0xCA62C1D6u; + } + else + { + throw new ArgumentException("Argument out of bounds! 0 <= t < 80", nameof(t)); + } + } + + /// + /// The circular left shift operation. + /// + /// An uint word. + /// 0 <= n < 32 + /// S^n(X) = (X << n) OR (X >> 32-n) + private static uint S(uint X, byte n) + { + if (n > 32) + { + throw new ArgumentOutOfRangeException(nameof(n)); + } + + return (X << n) | (X >> (32 - n)); + } + + /// + /// Ensures that given bytes are in big endian notation. + /// + /// An array of bytes + private static void EnsureBigEndian(ref byte[] array) + { + if (BitConverter.IsLittleEndian) + { + Array.Reverse(array); + } + } + + private readonly uint[] H = new uint[5]; + + private void Reset() + { + streamSize = 0; + messagePadded = false; + + // as defined in https://tools.ietf.org/html/rfc3174#section-6.1 + H[0] = 0x67452301u; + H[1] = 0xEFCDAB89u; + H[2] = 0x98BADCFEu; + H[3] = 0x10325476u; + H[4] = 0xC3D2E1F0u; + } + + public byte[] ComputeHash(byte[] message) + { + Reset(); + streamSize = 0; + PadMessage(ref message); + + ProcessBlock(message, 0, message.Length); + + return ProcessFinalBlock(); + } + + private void ProcessMultipleBlocks(byte[] message) + { + var messageCount = message.Length / BlockBytes; + for (var i = 0; i < messageCount; i += 1) + { + ProcessBlock(message, i * BlockBytes, BlockBytes); + } + } + + public byte[] ProcessFinalBlock() + { + if (!messagePadded) + { + var pad = new byte[0]; + PadMessage(ref pad, 0); + ProcessBlock(pad, 0, pad.Length); + } + + var digest = new byte[DigestBytes]; + for (int t = 0; t < H.Length; t++) + { + var hi = BitConverter.GetBytes(H[t]); + EnsureBigEndian(ref hi); + + Buffer.BlockCopy(hi, 0, digest, t * hi.Length, hi.Length); + } + + return digest; + } + + public void PadMessage(ref byte[] message, int length = 0) + { + if (messagePadded) + { + throw new InvalidOperationException(); + } + + if (length == 0) + { + length = message.Length; + } + else + { + Array.Resize(ref message, length); + } + + streamSize += length; + + var paddingBytes = BlockBytes - (length % BlockBytes); + + // 64bit uint message size will be appended to end of the padding, making sure we have space for it. + if (paddingBytes <= 8) + paddingBytes += BlockBytes; + + var padding = new byte[paddingBytes]; + padding[0] = 0b10000000; + + var messageBits = (ulong)streamSize << 3; + var messageSize = BitConverter.GetBytes(messageBits); + EnsureBigEndian(ref messageSize); + + Buffer.BlockCopy(messageSize, 0, padding, padding.Length - messageSize.Length, messageSize.Length); + + Array.Resize(ref message, message.Length + padding.Length); + Buffer.BlockCopy(padding, 0, message, length, padding.Length); + + messagePadded = true; + } + + public void ProcessBlock(byte[] message, int start, int length) + { + if (start + length > message.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + if (length % BlockBytes != 0) + { + throw new ArgumentException($"Invalid block size. Actual: {length}, Expected: Multiples of {BlockBytes}", nameof(length)); + } + if (length != BlockBytes) + { + ProcessMultipleBlocks(message); + return; + } + + streamSize += BlockBytes; + var W = new uint[80]; + + // Get W(0) .. W(15) + for (int t = 0; t <= 15; t++) + { + var wordBytes = new byte[sizeof(uint)]; + Buffer.BlockCopy(message, start + (t * sizeof(uint)), wordBytes, 0, sizeof(uint)); + EnsureBigEndian(ref wordBytes); + + W[t] = BitConverter.ToUInt32(wordBytes, 0); + } + + // Calculate W(16) .. W(79) + for (int t = 16; t <= 79; t++) + { + W[t] = S(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1); + } + + uint A = H[0], + B = H[1], + C = H[2], + D = H[3], + E = H[4]; + + for (int t = 0; t < 80; t++) + { + var temp = S(A, 5) + F(t, B, C, D) + E + W[t] + K(t); + E = D; + D = C; + C = S(B, 30); + B = A; + A = temp; + } + + H[0] += A; + H[1] += B; + H[2] += C; + H[3] += D; + H[4] += E; + } + } + } +} diff --git a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameRoundTripTests.cs b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameRoundTripTests.cs index 12bb5cd280..c6bfc854fa 100644 --- a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameRoundTripTests.cs +++ b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameRoundTripTests.cs @@ -839,7 +839,7 @@ public void Overloads18() string expectedManagedMethodName) { // Generate the fqn for the Reflection MethodInfo - ManagedNameHelper.GetManagedName(methodInfo, out var managedTypeName, out var managedMethodName); + ManagedNameHelper.GetManagedName(methodInfo, out var managedTypeName, out var managedMethodName, out var hierarchyValues); Assert.AreEqual(expectedManagedTypeName, managedTypeName); Assert.AreEqual(expectedManagedMethodName, managedMethodName); diff --git a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs new file mode 100644 index 0000000000..06220dc920 --- /dev/null +++ b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.TestPlatform.AdapterUtilities.UnitTests.TestIdProvider +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using System; + using System.Linq; + + [TestClass] + public class CompatibilityTests + { + [TestMethod] + [DataRow(new[] { "eea339da-6b5e-0d4b-3255-bfef95601890", "" })] + [DataRow(new[] { "740b9afc-3350-4257-ca01-5bd47799147d", "adapter://", "name1" })] // less than one block + [DataRow(new[] { "119c5b31-c0fb-1c12-6d1a-d617bb2bd996", "adapter://namesamplenam.testname" })] // 1 full block + [DataRow(new[] { "2a4c33ec-6115-4bd7-2e94-71f2fd3a5ee3", "adapter://namesamplenamespace.testname" })] // 1 full block and extra + [DataRow(new[] { "119c5b31-c0fb-1c12-6d1a-d617bb2bd996", "adapter://", "name", "samplenam", ".", "testname" })] // 1 full block + [DataRow(new[] { "2a4c33ec-6115-4bd7-2e94-71f2fd3a5ee3", "adapter://", "name", "samplenamespace", ".", "testname" })] // 1 full block and extra + [DataRow(new[] { "1fc07043-3d2d-1401-c732-3b507feec548", "adapter://namesamplenam.testnameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })] // 2 full blocks + [DataRow(new[] { "24e8a50b-2766-6a12-f461-9f8e4fa1cbb5", "adapter://namesamplenamespace.testnameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })] // 2 full blocks and extra + [DataRow(new[] { "1fc07043-3d2d-1401-c732-3b507feec548", "adapter://", "name", "samplenam", ".", "testname", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })] // 2 full blocks + [DataRow(new[] { "24e8a50b-2766-6a12-f461-9f8e4fa1cbb5", "adapter://", "name", "samplenamespace", ".", "testname", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })] // 2 full blocks and extra + public void IdCompatibilityTests(string[] data) + { + // Arrange + var expectedId = new Guid(data[0]); + + // Act + var idProvider = new AdapterUtilities.TestIdProvider(); + foreach (var d in data.Skip(1)) + { + idProvider.AppendString(d); + } + var id = idProvider.GetId(); + + // Assert + Assert.AreEqual(expectedId, id); + } + + + [TestMethod] + public void IdGeneration_TestVectors_EmptyString() + { + IdGeneration_TestVector( + string.Empty, + "eea339da-6b5e-0d4b-3255-bfef95601890" + ); + } + + + [TestMethod] + public void IdGeneration_TestVectors_abc() + { + IdGeneration_TestVector( + "abc", + "1af4049f-8584-1614-2050-e3d68c1a7abb" + ); + } + + [TestMethod] + public void IdGeneration_TestVectors_448Bits() + { + IdGeneration_TestVector( + "abcdbcdecdefdefgefghfghighij", + "7610f6db-8808-4bb7-b076-96871a96329c" + ); + } + + [TestMethod] + public void IdGeneration_TestVectors_896Bits() + { + IdGeneration_TestVector( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "76d8d751-c79a-402c-9c5b-0e3f69c60adc" + ); + } + + [TestMethod] + public void IdGeneration_TestVectors_1Block() + { + IdGeneration_TestRepetitionVector( + "a", 512 / 16, + "99b1aec7-ff50-5229-a378-70ca37914c90" + ); + } + + [TestMethod] + public void IdGeneration_ExtremelyLarge_TestVectors_100k_abc() + { + IdGeneration_TestRepetitionVector( + "abc", 100_000, + "11dbfc20-b34a-eef6-158e-ea8c201dfff9" + ); + } + + [TestMethod] + public void IdGeneration_ExtremelyLarge_TestVectors_10M_abc() + { + IdGeneration_TestRepetitionVector( + "abc", 10_000_000, + "78640f07-8041-71bd-6461-3a7e4db52389" + ); + } + + private void IdGeneration_TestVector(string testName, string expected) + { + // Arrange + expected = expected.Replace(" ", "").ToLowerInvariant(); + var idProvider = new AdapterUtilities.TestIdProvider(); + + // Act + idProvider.AppendString(testName); + var actual = idProvider.GetId().ToString(); + + // Assert + Assert.AreEqual(expected, actual, $"Test Id for '{testName}' is invalid!"); + } + + private void IdGeneration_TestRepetitionVector(string input, int repetition, string expected) + { + // Arrange + var idProvider = new AdapterUtilities.TestIdProvider(); + + // Act + for (int i = 0; i < repetition; i++) + { + idProvider.AppendString(input); + } + + var id = idProvider.GetId().ToString(); + + // Assert + Assert.AreEqual(expected, id, $"Test id generation for vector '{input}'*{repetition} failed! (normal path)"); + } + + } +} diff --git a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/SHA1ImplTests.cs b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/SHA1ImplTests.cs new file mode 100644 index 0000000000..23bb10bef8 --- /dev/null +++ b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/SHA1ImplTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.TestPlatform.AdapterUtilities.UnitTests.TestIdProvider +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using System; + using System.Linq; + using System.Text; + + [TestClass] + public class SHA1ImplTests + { + [TestMethod] + public void SHA1_TestVectors_EmptyString() + { + SHA1_TestVector( + string.Empty, + "da39a3ee5e6b4b0d3255bfef95601890afd80709" + ); + } + + [TestMethod] + public void SHA1_TestVectors_abc() + { + SHA1_TestVector( + "abc", + "a9993e364706816aba3e25717850c26c9cd0d89d" + ); + } + + [TestMethod] + public void SHA1_TestVectors_448Bits() + { + SHA1_TestVector( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "84983e441c3bd26ebaae4aa1f95129e5e54670f1" + ); + } + + [TestMethod] + public void SHA1_TestVectors_896Bits() + { + SHA1_TestVector( + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + "a49b2446a02c645bf419f995b67091253a04a259" + ); + } + + [TestMethod] + public void SHA1_TestVectors_1Block() + { + SHA1_TestRepetitionVector( + 'a', + 512 / 8 + ); + } + + [TestMethod] + public void SHA1_ExtremelyLarge_TestVectors_500k_a() + { + SHA1_TestRepetitionVector( + 'a', + 500_000 + ); + } + + [TestMethod] + public void SHA1_ExtremelyLarge_TestVectors_900k_a() + { + SHA1_TestRepetitionVector( + 'a', + 900_000 + ); + } + + [TestMethod] + public void SHA1_ExtremelyLarge_TestVectors_999999_a() + { + SHA1_TestRepetitionVector( + 'a', + 999_999 + ); + } + + [TestMethod] + public void SHA1_ExtremelyLarge_TestVectors_1M_a() + { + SHA1_TestRepetitionVector( + 'a', + 1_000_000, + "34aa973c d4c4daa4 f61eeb2b dbad2731 6534016f" + ); + } + + [TestMethod] + public void SHA1_ExtremelyLarge_TestVectors_10M_a() + { + SHA1_TestRepetitionVector( + 'a', + 10_000_000 + ); + } + + private void SHA1_TestVector(string message, string expected) + { + // Arrange + expected = expected.Replace(" ", "").ToLowerInvariant(); + var shaHasher1 = new AdapterUtilities.TestIdProvider.Sha1Implementation(); + + // Act + var bytes = UTF8Encoding.UTF8.GetBytes(message); + var digest1 = ToHex(shaHasher1.ComputeHash(bytes)); + + // Assert + Assert.AreEqual(expected, digest1, $"Test vector '{message}' failed!"); + } + + private void SHA1_TestRepetitionVector(char input, int repetition, string expected = null) + { + // Arrange + var shaHasher1 = new AdapterUtilities.TestIdProvider.Sha1Implementation(); + var shaHasher2 = new AdapterUtilities.TestIdProvider.Sha1Implementation(); + + var bytes = new byte[repetition]; + for (int i = 0; i < repetition; i++) + { + bytes[i] = (byte)input; + } + + if (string.IsNullOrEmpty(expected)) + { + using (var hasher = System.Security.Cryptography.SHA1.Create()) + { + expected = ToHex(hasher.ComputeHash(bytes)); + } + } + else + { + expected = expected.Replace(" ", "").ToLowerInvariant(); + } + + // Act + var digest1 = ToHex(shaHasher1.ComputeHash(bytes)); + var blocks = bytes.Length / AdapterUtilities.TestIdProvider.BlockBytes; + byte[] block; + for (var i = 0; i < blocks; i += 1) + { + block = new byte[AdapterUtilities.TestIdProvider.BlockBytes]; + Buffer.BlockCopy(bytes, i * block.Length, block, 0, block.Length); + shaHasher2.ProcessBlock(block, 0, block.Length); + } + + var rest = bytes.Length - blocks * AdapterUtilities.TestIdProvider.BlockBytes; + if (rest != 0) + { + block = new byte[rest]; + Buffer.BlockCopy(bytes, blocks * block.Length, block, 0, block.Length); + shaHasher2.PadMessage(ref block, block.Length); + shaHasher2.ProcessBlock(block, 0, block.Length); + } + + var digest2 = ToHex(shaHasher2.ProcessFinalBlock()); + + // Assert + Assert.AreEqual(expected, digest1, $"Test vector '{input}'*{repetition} failed! (normal path)"); + Assert.AreEqual(expected, digest2, $"Test vector '{input}'*{repetition} failed! (padding path)"); + } + + private static string ToHex(byte[] digest) => string.Concat(digest.Select(i => i.ToString("x2"))); + } +}