diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/Microsoft.TestPlatform.AdapterUtilities.csproj b/src/Microsoft.TestPlatform.AdapterUtilities/Microsoft.TestPlatform.AdapterUtilities.csproj index 5d34ff1083..59ddd05a82 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/Microsoft.TestPlatform.AdapterUtilities.csproj +++ b/src/Microsoft.TestPlatform.AdapterUtilities/Microsoft.TestPlatform.AdapterUtilities.csproj @@ -49,6 +49,10 @@ + + + + NullableHelpers.cs diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.TestPlatform.AdapterUtilities/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c58110..129935abda 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TestPlatform.AdapterUtilities/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2 +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2.AppendBytes(byte[]! bytes) -> void +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2.AppendString(string! str) -> void +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2.GetHash() -> byte[]! +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2.GetId() -> System.Guid +Microsoft.TestPlatform.AdapterUtilities.TestIdProvider2.TestIdProvider2() -> void diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs index fbefb86473..59648cc8ef 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs +++ b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider.cs @@ -8,8 +8,9 @@ namespace Microsoft.TestPlatform.AdapterUtilities; /// -/// Used to generate id for tests. +/// Used to generate id for tests using SHA1. /// +[Obsolete("TestIdProvider is deprecated and will soon be removed because it uses unsafe cryptographical hash SHA1 (for non-crypto purposes). Migrate to TestIdProvider2 that uses a non-cryptographical hash which is more appropriate for the task.")] public class TestIdProvider { private Guid _id = Guid.Empty; diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider2.cs b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider2.cs new file mode 100644 index 0000000000..227a44f0e1 --- /dev/null +++ b/src/Microsoft.TestPlatform.AdapterUtilities/TestIdProvider2.cs @@ -0,0 +1,116 @@ +// 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; +using System.IO.Hashing; +using System.Text; + +namespace Microsoft.TestPlatform.AdapterUtilities; +/// +/// Used to generate id for tests. +/// +public class TestIdProvider2 +{ + private Guid _id = Guid.Empty; + private byte[]? _hash; + + private readonly XxHash128 _xxhash; + + /// + /// Initializes a new instance of the class. + /// + public TestIdProvider2() + { + _xxhash = new XxHash128(); + } + + /// + /// Appends a string to id generation seed. + /// + /// String to append to the id seed. + /// Thrown if or is called already. + /// Thrown when is . + public void AppendString(string str) + { + if (_hash != null) + { + throw new InvalidOperationException(Resources.Resources.ErrorCannotAppendAfterHashCalculation); + } + _ = str ?? throw new ArgumentNullException(nameof(str)); + + var bytes = Encoding.Unicode.GetBytes(str); + + _xxhash.Append(bytes); + } + + /// + /// Appends an array of bytes to id generation seed. + /// + /// Array to append to the id seed. + /// Thrown if or is called already. + /// Thrown when is . + public void AppendBytes(byte[] bytes) + { + if (_hash != null) + { + throw new InvalidOperationException(Resources.Resources.ErrorCannotAppendAfterHashCalculation); + } + _ = bytes ?? throw new ArgumentNullException(nameof(bytes)); + + if (bytes.Length == 0) + { + return; + } + + _xxhash.Append(bytes); + } + + /// + /// Calculates the Id seed. + /// + /// An array containing the seed. + /// + /// and cannot be called + /// on instance after this method is called. + /// + public byte[] GetHash() + { + if (_hash != null) + { + return _hash; + } + + // Finalize the hash. We don't have any more data so we provide empty. + _hash = _xxhash.GetCurrentHash(); + + return _hash!; + } + + /// + /// Calculates the Id from the seed. + /// + /// Id + /// + /// and cannot be called + /// on instance after this method is called. + /// + public Guid GetId() + { + if (_id != Guid.Empty) + { + return _id; + } + +#if NET6_0_OR_GREATER + var hashSlice = GetHash().AsSpan().Slice(0, 16); + _id = new Guid(hashSlice); +#else + // create from span? + var toGuid = new byte[16]; + Array.Copy(GetHash(), toGuid, 16); + _id = new Guid(toGuid); +#endif + + return _id; + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj index 1675d4cb00..b588f3c9d2 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj +++ b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj @@ -39,6 +39,10 @@ + + + + TextTemplatingFileGenerator diff --git a/src/Microsoft.TestPlatform.ObjectModel/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.TestPlatform.ObjectModel/PublicAPI/PublicAPI.Unshipped.txt index 343eac0f8d..3f7a0092a2 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TestPlatform.ObjectModel/PublicAPI/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ Microsoft.VisualStudio.TestPlatform.ObjectModel.TelemetryEvent Microsoft.VisualStudio.TestPlatform.ObjectModel.TelemetryEvent.Name.get -> string! Microsoft.VisualStudio.TestPlatform.ObjectModel.TelemetryEvent.Properties.get -> System.Collections.Generic.IDictionary! Microsoft.VisualStudio.TestPlatform.ObjectModel.TelemetryEvent.TelemetryEvent(string! name, System.Collections.Generic.IDictionary! properties) -> void +static Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities.EqtHash.GuidFromString2(string! data) -> System.Guid diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestCase.cs b/src/Microsoft.TestPlatform.ObjectModel/TestCase.cs index b9c260fb9f..a8a9cfe9e9 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestCase.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestCase.cs @@ -201,7 +201,7 @@ private Guid GetTestId() // If ManagedType and ManagedMethod properties are filled than TestId should be based on those. testcaseFullName += GetFullyQualifiedName(); - return EqtHash.GuidFromString(testcaseFullName); + return EqtHash.GuidFromString2(testcaseFullName); } private void SetVariableAndResetId(ref T variable, T value) diff --git a/src/Microsoft.TestPlatform.ObjectModel/Utilities/EqtHash.cs b/src/Microsoft.TestPlatform.ObjectModel/Utilities/EqtHash.cs index ef595cffb5..9b7a018851 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/Utilities/EqtHash.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/Utilities/EqtHash.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.IO.Hashing; +using System.Security.Cryptography; using Microsoft.VisualStudio.TestPlatform.CoreUtilities; @@ -17,6 +19,8 @@ public static class EqtHash /// Calculates a hash of the string and copies the first 128 bits of the hash /// to a new Guid. /// + [Obsolete("GuidFromString is deprecated and will soon be removed because it uses unsafe cryptographical hash SHA1 (for non-crypto purposes). Migrate to GuidFromString2 that uses a non-cryptographical hash which is more appropriate for the task.")] + public static Guid GuidFromString(string data) { TPDebug.Assert(data != null); @@ -27,7 +31,8 @@ public static Guid GuidFromString(string data) // Any algorithm or logic change must require a sign off from feature owners of above // Also, TPV2 and TPV1 must use same Algorithm until the time TPV1 is completely deleted to be on-par // If LUT or .Net core scenario uses TPV2 to discover, but if it uses TPV1 in Devenv, then there will be testcase matching issues - byte[] hash = Sha1Helper.ComputeSha1(System.Text.Encoding.Unicode.GetBytes(data)); + using HashAlgorithm provider = SHA1.Create(); + byte[] hash = provider.ComputeHash(System.Text.Encoding.Unicode.GetBytes(data)); // Guid is always 16 bytes TPDebug.Assert(Guid.Empty.ToByteArray().Length == 16, "Expected Guid to be 16 bytes"); @@ -37,4 +42,27 @@ public static Guid GuidFromString(string data) return new Guid(toGuid); } + + /// + /// Calculates a hash of the string and copies the first 128 bits of the hash + /// to a new Guid. + /// + public static Guid GuidFromString2(string data) + { + TPDebug.Assert(data != null); + + byte[] hash = XxHash128.Hash(System.Text.Encoding.Unicode.GetBytes(data)); + + // Guid is always 16 bytes + TPDebug.Assert(Guid.Empty.ToByteArray().Length == 16, "Expected Guid to be 16 bytes"); + +#if NET6_0_OR_GREATER + return new Guid(hash.AsSpan().Slice(0, 16)); +#else + // create from span? + var toGuid = new byte[16]; + Array.Copy(hash, toGuid, 16); + return new Guid(toGuid); +#endif + } } diff --git a/src/Microsoft.TestPlatform.ObjectModel/Utilities/Sha1Helper.cs b/src/Microsoft.TestPlatform.ObjectModel/Utilities/Sha1Helper.cs deleted file mode 100644 index 7b2a249fa3..0000000000 --- a/src/Microsoft.TestPlatform.ObjectModel/Utilities/Sha1Helper.cs +++ /dev/null @@ -1,230 +0,0 @@ -// 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; -using System.Security.Cryptography; - -namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; - -/// -/// Used to calculate SHA1 hash. -/// -/// https://tools.ietf.org/html/rfc3174 -/// -internal static class Sha1Helper -{ - public static byte[] ComputeSha1(byte[] message) - { - using HashAlgorithm provider = SHA1.Create(); - byte[] hash = provider.ComputeHash(message); - - return hash; - } - - /// - /// 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 const int BlockBits = 512; - private const int DigestBits = 160; - private const int BlockBytes = BlockBits / 8; - private const int DigestBytes = DigestBits / 8; - - /// - /// 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) - { - return t switch - { - >= 0 and <= 19 => b & c | ~b & d, - >= 20 and <= 39 or >= 60 and <= 79 => b ^ c ^ d, - _ => t is >= 40 and <= 59 - ? b & c | b & d | c & d - : 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) - { - return t switch - { - >= 0 and <= 19 => 0x5A827999u, - >= 20 and <= 39 => 0x6ED9EBA1u, - >= 40 and <= 59 => 0x8F1BBCDCu, - _ => t is >= 60 and <= 79 - ? 0xCA62C1D6u - : 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) - { - return n > 32 ? throw new ArgumentOutOfRangeException(nameof(n)) : (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) - { - ValidateArg.NotNull(array, nameof(array)); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(array); - } - } - - private readonly uint[] _h = new uint[5]; - - private void Reset() - { - // 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) - { - ValidateArg.NotNull(message, nameof(message)); - - Reset(); - PadMessage(ref message); - - var messageCount = message.Length / BlockBytes; - for (var i = 0; i < messageCount; ++i) - { - ProcessBlock(message, i * BlockBytes, BlockBytes); - } - - 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; - } - - private static void PadMessage(ref byte[] message) - { - var length = message.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)message.Length << 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); - } - - private void ProcessBlock(byte[] message, int start, int length) - { - if (start + length > message.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - if (length != BlockBytes) - { - throw new ArgumentException($"Invalid block size. Actual: {length}, Expected: {BlockBytes}", nameof(length)); - } - - 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/TestIdProvider/CompatibilityTests.cs b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs index 175ea9bcb2..b22b822bb2 100644 --- a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs +++ b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/TestIdProvider/CompatibilityTests.cs @@ -9,6 +9,7 @@ namespace Microsoft.TestPlatform.AdapterUtilities.UnitTests.TestIdProvider; [TestClass] +[Obsolete("Testing obsolete api that we did not remove yet.")] public class CompatibilityTests { [TestMethod]