File: Utilities\EqtHash.cs
Web Access
Project: Microsoft.TestPlatform.ObjectModel.csproj (Microsoft.VisualStudio.TestPlatform.ObjectModel)
// 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;
using System.Text;

using Microsoft.TestPlatform.Hashing;
using Microsoft.VisualStudio.TestPlatform.CoreUtilities;

namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;

/// <summary>
/// Wrapper class for hashing.
/// </summary>
public static class EqtHash
{
    /// <summary>
    /// Calculates a SHA1 hash of the string and copies the first 128 bits of the hash
    /// to a new Guid.
    /// </summary>
    /// <remarks>
    /// This is the algorithm test case ids are computed with by default. See
    /// <see cref="GuidFromStringXxHash128(string)"/> for the xxHash128 based successor, which is available
    /// but not yet the default.
    /// </remarks>
    public static Guid GuidFromString(string data)
    {
        TPDebug.Assert(data != null);

        // Do NOT change the algorithm ever as this will have compat implications
        // TC-TA team has a feature in VS where workitems are associated based on TestCase Ids
        // If Algorithm changes, then all the bugs/workitems filed in TFS Server against a given TestCase become unassociated if IDs change
        // 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
        using HashAlgorithm provider = SHA1.Create();
        byte[] hash = provider.ComputeHash(Encoding.Unicode.GetBytes(data));

        // Guid is always 16 bytes
        TPDebug.Assert(Guid.Empty.ToByteArray().Length == 16, "Expected Guid to be 16 bytes");

        byte[] toGuid = new byte[16];
        Array.Copy(hash, toGuid, 16);

        return new Guid(toGuid);
    }

    /// <summary>
    /// Calculates an xxHash128 hash of the string and turns it into an RFC 9562 version 8 UUID.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This is the intended successor to <see cref="GuidFromString(string)"/>. xxHash128 is a
    /// non-cryptographic hash, which is what this has always needed - the id is an identity, never a
    /// security boundary - and it is considerably faster than SHA1. Using a cryptographic hash for
    /// this also makes vstest show up in security scans that flag SHA1 regardless of how it is used.
    /// </para>
    /// <para>
    /// The resulting Guid carries the version of the hashing scheme in its top 4 bits, so ids
    /// produced by this method are distinguishable from legacy SHA1 ids and from any future
    /// scheme. See TestIdGuid.
    /// </para>
    /// <para>
    /// This deliberately does NOT produce the same value as <see cref="GuidFromString(string)"/>.
    /// Changing the id of a test is a breaking change for anything that stored it, which is why this
    /// ships available but not default: test case ids are computed with it only when a run opts in
    /// through the VSTEST_DISABLE_XXHASH128_TESTCASE_ID feature flag. The warning on
    /// <see cref="GuidFromString(string)"/> about work item association requiring sign off from the
    /// TC-TA feature owners applies to making this the default.
    /// </para>
    /// <para>
    /// The bytes are laid out exactly as MSTest lays them out, so the same input produces the same
    /// id in both, and an id produced by either is legible to the other.
    /// </para>
    /// </remarks>
    public static Guid GuidFromStringXxHash128(string data)
    {
        TPDebug.Assert(data != null);

        // An xxHash128 hash is 16 bytes, exactly the size of a Guid.
        byte[] hash = XxHash128.Hash(Encoding.Unicode.GetBytes(data));

        TPDebug.Assert(Guid.Empty.ToByteArray().Length == 16, "Expected Guid to be 16 bytes");

        return TestIdGuid.VersionedGuidFromHash(hash, TestIdGuid.CurrentHashVersion);
    }
}