< Summary

Information
Class: SwiftCollections.SwiftHashTools
Assembly: SwiftCollections
File(s): /home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Utility/SwiftHashTools.cs
Line coverage
100%
Covered lines: 118
Uncovered lines: 0
Coverable lines: 118
Total lines: 445
Line coverage: 100%
Branch coverage
98%
Covered branches: 59
Total branches: 60
Branch coverage: 98.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Utility/SwiftHashTools.cs

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using System.Runtime.CompilerServices;
 5using System.Runtime.Serialization;
 6using System.Security.Cryptography;
 7using System.Threading;
 8
 9namespace SwiftCollections;
 10
 11/// <summary>
 12/// Provides utility functions for generating, combining, and working with hash codes.
 13/// </summary>
 14public static class SwiftHashTools
 15{
 16    internal const int DefaultDeterministicStringHashSeed = 0x51C4B5D;
 17    internal const int DefaultDeterministicObjectHashSeed = 0x1F6E4D75;
 18
 119    private static readonly IEqualityComparer<string> s_deterministicStringComparer =
 120        new SwiftDeterministicStringEqualityComparer(DefaultDeterministicStringHashSeed);
 21
 122    private static readonly IEqualityComparer<object> s_deterministicObjectComparer =
 123        new SwiftDeterministicObjectEqualityComparer(DefaultDeterministicObjectHashSeed);
 24
 25    /// <summary>
 26    /// Provides a cryptographic random number generator for collision-hardening entropy.
 27    /// </summary>
 28    private static RandomNumberGenerator? rng;
 29
 30    /// <summary>
 31    /// Stores random bytes used for entropy generation.
 32    /// </summary>
 33    private static byte[]? data;
 34
 35    /// <summary>
 36    /// Tracks the current index in the entropy data buffer.
 37    /// </summary>
 138    private static int currentIndex = 1024;
 39
 40    /// <summary>
 41    /// Synchronization object for thread-safe operations.
 42    /// </summary>
 143    private static readonly object lockObj = new();
 44
 45    /// <summary>
 46    /// Holds serialization information for objects during the serialization process.
 47    /// </summary>
 48    private static ConditionalWeakTable<object, SerializationInfo>? s_SerializationInfoTable;
 49
 50    /// <summary>
 51    /// Gets the table that stores serialization information for objects.
 52    /// </summary>
 53    internal static ConditionalWeakTable<object, SerializationInfo> SerializationInfoTable
 54    {
 55        get
 56        {
 257            if (s_SerializationInfoTable == null)
 58            {
 159                ConditionalWeakTable<object, SerializationInfo> value = new();
 160                Interlocked.CompareExchange(ref s_SerializationInfoTable, value, null);
 61            }
 62
 263            return s_SerializationInfoTable;
 64        }
 65    }
 66
 67    /// <summary>
 68    /// Determines whether the specified integer is a power of two.
 69    /// </summary>
 70    /// <remarks>Zero and negative values are not considered powers of two.</remarks>
 71    /// <param name="x">The integer value to test.</param>
 72    /// <returns>true if the value of x is a power of two; otherwise, false.</returns>
 73    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 474    public static bool IsPowerOfTwo(int x) => (x != 0) && ((x & (x - 1)) == 0);
 75
 76    /// <summary>
 77    /// Calculates the smallest power of two that is greater than or equal to the specified integer.
 78    /// This method is used to ensure that capacities are powers of two, enabling optimizations
 79    /// in indexing operations through bitwise arithmetic.
 80    /// </summary>
 81    /// <param name="value">The integer value for which to find the next power of two.</param>
 82    /// <returns>The smallest power of two greater than or equal to <paramref name="value"/>.</returns>
 83    /// <remarks>
 84    /// If <paramref name="value"/> is less than or equal to zero, the method returns 1.
 85    /// If <paramref name="value"/> is too large to represent as a power of two within an <c>int</c>,
 86    /// the methods returns `int.Max`.
 87    /// </remarks>
 88    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 89    public static int NextPowerOfTwo(int value)
 90    {
 1203291        if (value <= 0) return 1;
 1202992        if (value >= (1 << 30)) return int.MaxValue;
 93
 1202794        value--;
 1202795        value |= value >> 1;
 1202796        value |= value >> 2;
 1202797        value |= value >> 4;
 1202798        value |= value >> 8;
 1202799        value |= value >> 16;
 12027100        value++;
 12027101        return value;
 102    }
 103
 104    /// <summary>
 105    /// Determines whether the specified comparer is a recognized, well-known equality comparer supported by the system.
 106    /// </summary>
 107    /// <remarks>
 108    /// A well-known equality comparer is one that is commonly used and recognized by the system,
 109    /// such as the default equality comparers for string and object, or specific deterministic comparers.
 110    /// Use this method to check if a comparer is supported for optimized or special handling.
 111    /// </remarks>
 112    /// <param name="comparer">
 113    /// The object to test as an equality comparer.
 114    /// This can be null or an instance of a supported equality comparer type.
 115    /// </param>
 116    /// <returns>true if the comparer is a well-known equality comparer; otherwise, false.</returns>
 117    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 118    public static bool IsWellKnownEqualityComparer(object comparer)
 119    {
 8120        return comparer == null
 8121            || comparer == EqualityComparer<string>.Default
 8122            || comparer == EqualityComparer<object>.Default
 8123            || comparer is SwiftDeterministicStringEqualityComparer
 8124            || comparer is SwiftDeterministicObjectEqualityComparer;
 125    }
 126
 127    /// <summary>
 128    /// Gets the default comparer used by SwiftCollections for the specified type when no comparer is supplied.
 129    /// String keys are hashed deterministically. Object keys hash strings deterministically, while other object-key
 130    /// determinism still depends on the underlying key type's <see cref="object.GetHashCode()"/> implementation.
 131    /// </summary>
 132    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 133    public static IEqualityComparer<T> GetDeterministicEqualityComparer<T>()
 134    {
 404135        if (typeof(T) == typeof(string))
 87136            return (IEqualityComparer<T>)GetDeterministicStringEqualityComparer();
 137
 317138        if (typeof(T) == typeof(object))
 6139            return (IEqualityComparer<T>)GetDeterministicObjectEqualityComparer();
 140
 311141        return EqualityComparer<T>.Default;
 142    }
 143
 144    /// <summary>
 145    /// Gets the deterministic default comparer used by SwiftCollections for strings.
 146    /// </summary>
 147    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 148    public static IEqualityComparer<string> GetDeterministicStringEqualityComparer()
 88149        => s_deterministicStringComparer;
 150
 151    /// <summary>
 152    /// Gets a deterministic string comparer using the supplied seed.
 153    /// </summary>
 154    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 155    public static IEqualityComparer<string> GetDeterministicStringEqualityComparer(int seed)
 2156        => seed == DefaultDeterministicStringHashSeed
 2157            ? s_deterministicStringComparer
 2158            : new SwiftDeterministicStringEqualityComparer(seed);
 159
 160    /// <summary>
 161    /// Gets the default comparer used by SwiftCollections for object keys.
 162    /// Strings are hashed deterministically even when stored as objects. Other object keys still depend on their
 163    /// runtime <see cref="object.GetHashCode()"/> implementations.
 164    /// </summary>
 165    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 166    public static IEqualityComparer<object> GetDeterministicObjectEqualityComparer()
 7167        => s_deterministicObjectComparer;
 168
 169    /// <summary>
 170    /// Gets an object comparer using the supplied seed.
 171    /// Strings are hashed deterministically even when stored as objects. Other object keys still depend on their
 172    /// runtime <see cref="object.GetHashCode()"/> implementations.
 173    /// </summary>
 174    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 175    public static IEqualityComparer<object> GetDeterministicObjectEqualityComparer(int seed)
 2176        => seed == DefaultDeterministicObjectHashSeed
 2177            ? s_deterministicObjectComparer
 2178            : new SwiftDeterministicObjectEqualityComparer(seed);
 179
 180    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 181    internal static IEqualityComparer<T> GetDefaultEqualityComparer<T>(IEqualityComparer<T>? comparer = null)
 394182        => comparer ?? GetDeterministicEqualityComparer<T>();
 183
 184    /// <summary>
 185    /// Returns an IEqualityComparer instance optimized for use with Swift serialization, based on the specified compare
 186    /// </summary>
 187    /// <remarks>
 188    /// Use this method to obtain an equality comparer that ensures deterministic behavior when serializing with Swift.
 189    /// If the provided comparer is not recognized, a default Swift object equality comparer is returned.
 190    /// </remarks>
 191    /// <param name="comparer">
 192    /// The comparer object to evaluate.
 193    /// Determines which specialized Swift equality comparer to return.
 194    /// Can be an EqualityComparer for string or object, or a custom Swift deterministic comparer.
 195    /// </param>
 196    /// <returns>
 197    /// An IEqualityComparer instance suitable for Swift serialization.
 198    /// Returns a specialized comparer for strings or objects, depending on the input.
 199    /// </returns>
 200    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 201    public static IEqualityComparer GetSwiftEqualityComparer(object comparer)
 202    {
 7203        if (comparer == EqualityComparer<string>.Default || comparer is SwiftDeterministicStringEqualityComparer)
 4204            return new SwiftStringEqualityComparer();
 205
 3206        if (comparer == EqualityComparer<object>.Default || comparer is SwiftDeterministicObjectEqualityComparer)
 3207            return new SwiftObjectEqualityComparer();
 208
 209        return new SwiftObjectEqualityComparer();
 210    }
 211
 212    /// <summary>
 213    /// Generates a cryptographically strong random 64-bit integer for collision-hardening entropy.
 214    /// </summary>
 215    /// <returns>A 64-bit integer filled with cryptographically strong random bytes.</returns>
 216    internal static long GetEntropy()
 217    {
 139218        lock (lockObj)
 219        {
 139220            if (currentIndex == 1024 || rng == null || data == null)
 221            {
 2222                rng ??= RandomNumberGenerator.Create();
 2223                data ??= new byte[1024];
 224
 2225                rng.GetBytes(data);
 2226                currentIndex = 0;
 227            }
 228
 139229            long result = BitConverter.ToInt64(data, currentIndex);
 139230            currentIndex += 8;
 139231            return result;
 232        }
 139233    }
 234
 235    #region Hash Code Combination
 236
 237    /// <summary>
 238    /// Combines the hash codes of the elements in a tuple using a DJB2-inspired mixing strategy.
 239    /// </summary>
 240    public static int CombineHashCodes(
 241        this ITuple tupled,
 242        int seed = 5381,
 243        int shift1 = 16,
 244        int shift2 = 5,
 245        int shift3 = 27,
 246        int factor3 = 1566083941)
 247    {
 1248        int hash1 = (seed << shift1) + seed;
 1249        int hash2 = hash1;
 250
 10251        for (int i = 0; i < tupled.Length; i++)
 252        {
 4253            int itemHash = tupled[i]?.GetHashCode() ?? 0;
 254            unchecked
 255            {
 4256                if (i % 2 == 0)
 2257                    hash1 = ((hash1 << shift2) + hash1 + (hash1 >> shift3)) ^ itemHash;
 258                else
 2259                    hash2 = ((hash2 << shift2) + hash2 + (hash2 >> shift3)) ^ itemHash;
 260            }
 261        }
 262
 1263        return hash1 ^ (hash2 * factor3);
 264    }
 265
 266    /// <summary>
 267    /// Combines the hash codes of a set of objects using a DJB2-inspired mixing strategy.
 268    /// </summary>
 269    public static int CombineHashCodes(
 270        this object[] values,
 271        int seed = 5381,
 272        int shift1 = 16,
 273        int shift2 = 5,
 274        int shift3 = 27,
 275        int factor3 = 1566083941)
 276    {
 7277        int hash1 = (seed << shift1) + seed;
 7278        int hash2 = hash1;
 279
 62280        for (int i = 0; i < values.Length; i++)
 281        {
 24282            var itemHash = values[i]?.GetHashCode() ?? 0;
 283            unchecked
 284            {
 24285                if ((i & 1) == 0)
 13286                    hash1 = ((hash1 << shift2) + hash1 + (hash1 >> shift3)) ^ itemHash;
 287                else
 11288                    hash2 = ((hash2 << shift2) + hash2 + (hash2 >> shift3)) ^ itemHash;
 289            }
 290        }
 291
 7292        return hash1 + (hash2 * factor3);
 293    }
 294
 295    /// <summary>
 296    /// Combines the hash codes of the provided objects using a DJB2-inspired mixing strategy.
 297    /// </summary>
 6298    public static int CombineHashCodes(params object[] values) => values.CombineHashCodes();
 299
 300    /// <summary>
 301    /// Combines two integer hash codes without allocating the object-array params path.
 302    /// </summary>
 303    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 304    public static int CombineHashCodes(
 305        int value1,
 306        int value2)
 307    {
 308        const int seed = 5381;
 309        const int shift1 = 16;
 310        const int shift2 = 5;
 311        const int shift3 = 27;
 312        const int factor3 = 1566083941;
 1313        int hash1 = (seed << shift1) + seed;
 1314        int hash2 = hash1;
 315
 1316        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1317        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 318
 1319        return hash1 + (hash2 * factor3);
 320    }
 321
 322    /// <summary>
 323    /// Combines three integer hash codes without allocating the object-array params path.
 324    /// </summary>
 325    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 326    public static int CombineHashCodes(
 327        int value1,
 328        int value2,
 329        int value3)
 330    {
 331        const int seed = 5381;
 332        const int shift1 = 16;
 333        const int shift2 = 5;
 334        const int shift3 = 27;
 335        const int factor3 = 1566083941;
 1042336        int hash1 = (seed << shift1) + seed;
 1042337        int hash2 = hash1;
 338
 1042339        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1042340        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 1042341        hash1 = MixHashCode(hash1, value3, shift2, shift3);
 342
 1042343        return hash1 + (hash2 * factor3);
 344    }
 345
 346    /// <summary>
 347    /// Combines four integer hash codes without allocating the object-array params path.
 348    /// </summary>
 349    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 350    public static int CombineHashCodes(
 351        int value1,
 352        int value2,
 353        int value3,
 354        int value4)
 355    {
 356        const int seed = 5381;
 357        const int shift1 = 16;
 358        const int shift2 = 5;
 359        const int shift3 = 27;
 360        const int factor3 = 1566083941;
 1361        int hash1 = (seed << shift1) + seed;
 1362        int hash2 = hash1;
 363
 1364        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1365        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 1366        hash1 = MixHashCode(hash1, value3, shift2, shift3);
 1367        hash2 = MixHashCode(hash2, value4, shift2, shift3);
 368
 1369        return hash1 + (hash2 * factor3);
 370    }
 371
 372    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 373    private static int MixHashCode(int hash, int itemHash, int shift2, int shift3)
 374    {
 375        unchecked
 376        {
 3132377            return ((hash << shift2) + hash + (hash >> shift3)) ^ itemHash;
 378        }
 379    }
 380
 381    /// <summary>
 382    /// Computes a hash code for a string using the MurmurHash3 algorithm, incorporating entropy for randomization.
 383    /// </summary>
 384    /// <param name="key">The string to hash.</param>
 385    /// <param name="seed">The seed value for the hash function.</param>
 386    /// <returns>An integer hash code for the string.</returns>
 387    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 388    public static unsafe int MurmurHash3(string key, int seed)
 389    {
 390        const uint c1 = 0xcc9e2d51;
 391        const uint c2 = 0x1b873593;
 308338392        uint h1 = (uint)seed;
 393
 308338394        fixed (char* ptr = key)
 395        {
 308338396            int length = key.Length;
 308338397            int blockCount = length / 2;
 398
 399            unchecked
 400            {
 401                // Process 32-bit blocks
 7849390402                for (int i = 0; i < blockCount; i++)
 403                {
 3616357404                    uint k1 = *(uint*)(ptr + (i * 2));
 3616357405                    k1 *= c1;
 3616357406                    k1 = RotateLeft(k1, 15);
 3616357407                    k1 *= c2;
 3616357408                    h1 ^= k1;
 3616357409                    h1 = RotateLeft(h1, 13);
 3616357410                    h1 = h1 * 5 + 0xe6546b64;
 411                }
 412
 413                // Handle remaining characters if odd length
 308338414                if (length % 2 != 0)
 415                {
 25799416                    uint k1 = ptr[length - 1];
 25799417                    k1 *= c1;
 25799418                    k1 = RotateLeft(k1, 15);
 25799419                    k1 *= c2;
 25799420                    h1 ^= k1;
 421                }
 422            }
 423
 308338424            h1 ^= (uint)length;
 308338425            h1 = FMix(h1);
 308338426            return (int)(h1 & 0x7FFFFFFF);
 427        }
 428    }
 429
 430    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 7258513431    private static uint RotateLeft(uint x, int r) => (x << r) | (x >> (32 - r));
 432
 433    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 434    private static uint FMix(uint h)
 435    {
 308338436        h ^= h >> 16;
 308338437        h *= 0x85ebca6b;
 308338438        h ^= h >> 13;
 308338439        h *= 0xc2b2ae35;
 308338440        h ^= h >> 16;
 308338441        return h;
 442    }
 443
 444    #endregion
 445}