File: Hashing\NonCryptographicHashAlgorithm.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
// Copied from https://github.com/dotnet/runtime/blob/f1d463f46268382a8287d71aea929dadaa5dfef5/src/libraries/System.IO.Hashing/src/System/IO/Hashing/NonCryptographicHashAlgorithm.cs#L1
// Remove once we can actually add a reference to System.IO.Hashing v8.0.0
// <auto-generated/>
 
using System.Buffers;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.IO.Hashing
{
    /// <summary>
    ///   Represents a non-cryptographic hash algorithm.
    /// </summary>
    internal abstract class NonCryptographicHashAlgorithm
    {
        /// <summary>
        ///   Gets the number of bytes produced from this hash algorithm.
        /// </summary>
        /// <value>The number of bytes produced from this hash algorithm.</value>
        public int HashLengthInBytes { get; }
 
        /// <summary>
        ///   Called from constructors in derived classes to initialize the
        ///   <see cref="NonCryptographicHashAlgorithm"/> class.
        /// </summary>
        /// <param name="hashLengthInBytes">
        ///   The number of bytes produced from this hash algorithm.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="hashLengthInBytes"/> is less than 1.
        /// </exception>
        protected NonCryptographicHashAlgorithm(int hashLengthInBytes)
        {
            if (hashLengthInBytes < 1)
                throw new ArgumentOutOfRangeException(nameof(hashLengthInBytes));
 
            HashLengthInBytes = hashLengthInBytes;
        }
 
        /// <summary>
        ///   When overridden in a derived class,
        ///   appends the contents of <paramref name="source"/> to the data already
        ///   processed for the current hash computation.
        /// </summary>
        /// <param name="source">The data to process.</param>
        public abstract void Append(ReadOnlySpan<byte> source);
 
        /// <summary>
        ///   When overridden in a derived class,
        ///   resets the hash computation to the initial state.
        /// </summary>
        public abstract void Reset();
 
        /// <summary>
        ///   When overridden in a derived class,
        ///   writes the computed hash value to <paramref name="destination"/>
        ///   without modifying accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <remarks>
        ///   <para>
        ///     Implementations of this method must write exactly
        ///     <see cref="HashLengthInBytes"/> bytes to <paramref name="destination"/>.
        ///     Do not assume that the buffer was zero-initialized.
        ///   </para>
        ///   <para>
        ///     The <see cref="NonCryptographicHashAlgorithm"/> class validates the
        ///     size of the buffer before calling this method, and slices the span
        ///     down to be exactly <see cref="HashLengthInBytes"/> in length.
        ///   </para>
        /// </remarks>
        protected abstract void GetCurrentHashCore(Span<byte> destination);
 
        /// <summary>
        ///   Appends the contents of <paramref name="source"/> to the data already
        ///   processed for the current hash computation.
        /// </summary>
        /// <param name="source">The data to process.</param>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="source"/> is <see langword="null"/>.
        /// </exception>
        public void Append(byte[] source)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }
 
            Append(new ReadOnlySpan<byte>(source));
        }
 
        /// <summary>
        ///   Appends the contents of <paramref name="stream"/> to the data already
        ///   processed for the current hash computation.
        /// </summary>
        /// <param name="stream">The data to process.</param>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="stream"/> is <see langword="null"/>.
        /// </exception>
        /// <seealso cref="AppendAsync(Stream, CancellationToken)"/>
        public void Append(Stream stream)
        {
            if (stream is null)
            {
                throw new ArgumentNullException(nameof(stream));
            }
 
            byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
 
            while (true)
            {
                int read = stream.Read(buffer, 0, buffer.Length);
 
                if (read == 0)
                {
                    break;
                }
 
                Append(new ReadOnlySpan<byte>(buffer, 0, read));
            }
 
            ArrayPool<byte>.Shared.Return(buffer);
        }
 
        /// <summary>
        ///   Asychronously reads the contents of <paramref name="stream"/>
        ///   and appends them to the data already
        ///   processed for the current hash computation.
        /// </summary>
        /// <param name="stream">The data to process.</param>
        /// <param name="cancellationToken">
        ///   The token to monitor for cancellation requests.
        ///   The default value is <see cref="CancellationToken.None"/>.
        /// </param>
        /// <returns>
        ///   A task that represents the asynchronous append operation.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="stream"/> is <see langword="null"/>.
        /// </exception>
        public Task AppendAsync(Stream stream, CancellationToken cancellationToken = default)
        {
            if (stream is null)
            {
                throw new ArgumentNullException(nameof(stream));
            }
 
            return AppendAsyncCore(stream, cancellationToken);
        }
 
        private async Task AppendAsyncCore(Stream stream, CancellationToken cancellationToken)
        {
            byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
 
            while (true)
            {
#if NET
                int read = await stream.ReadAsync(buffer.AsMemory(), cancellationToken).ConfigureAwait(false);
#else
                int read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
#endif
 
                if (read == 0)
                {
                    break;
                }
 
                Append(new ReadOnlySpan<byte>(buffer, 0, read));
            }
 
            ArrayPool<byte>.Shared.Return(buffer);
        }
 
        /// <summary>
        ///   Gets the current computed hash value without modifying accumulated state.
        /// </summary>
        /// <returns>
        ///   The hash value for the data already provided.
        /// </returns>
        public byte[] GetCurrentHash()
        {
            byte[] ret = new byte[HashLengthInBytes];
            GetCurrentHashCore(ret);
            return ret;
        }
 
        /// <summary>
        ///   Attempts to write the computed hash value to <paramref name="destination"/>
        ///   without modifying accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <param name="bytesWritten">
        ///   On success, receives the number of bytes written to <paramref name="destination"/>.
        /// </param>
        /// <returns>
        ///   <see langword="true"/> if <paramref name="destination"/> is long enough to receive
        ///   the computed hash value; otherwise, <see langword="false"/>.
        /// </returns>
        public bool TryGetCurrentHash(Span<byte> destination, out int bytesWritten)
        {
            if (destination.Length < HashLengthInBytes)
            {
                bytesWritten = 0;
                return false;
            }
 
            GetCurrentHashCore(destination.Slice(0, HashLengthInBytes));
            bytesWritten = HashLengthInBytes;
            return true;
        }
 
        /// <summary>
        ///   Writes the computed hash value to <paramref name="destination"/>
        ///   without modifying accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <returns>
        ///   The number of bytes written to <paramref name="destination"/>,
        ///   which is always <see cref="HashLengthInBytes"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        ///   <paramref name="destination"/> is shorter than <see cref="HashLengthInBytes"/>.
        /// </exception>
        public int GetCurrentHash(Span<byte> destination)
        {
            if (destination.Length < HashLengthInBytes)
            {
                ThrowDestinationTooShort();
            }
 
            GetCurrentHashCore(destination.Slice(0, HashLengthInBytes));
            return HashLengthInBytes;
        }
 
        /// <summary>
        ///   Gets the current computed hash value and clears the accumulated state.
        /// </summary>
        /// <returns>
        ///   The hash value for the data already provided.
        /// </returns>
        public byte[] GetHashAndReset()
        {
            byte[] ret = new byte[HashLengthInBytes];
            GetHashAndResetCore(ret);
            return ret;
        }
 
        /// <summary>
        ///   Attempts to write the computed hash value to <paramref name="destination"/>.
        ///   If successful, clears the accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <param name="bytesWritten">
        ///   On success, receives the number of bytes written to <paramref name="destination"/>.
        /// </param>
        /// <returns>
        ///   <see langword="true"/> and clears the accumulated state
        ///   if <paramref name="destination"/> is long enough to receive
        ///   the computed hash value; otherwise, <see langword="false"/>.
        /// </returns>
        public bool TryGetHashAndReset(Span<byte> destination, out int bytesWritten)
        {
            if (destination.Length < HashLengthInBytes)
            {
                bytesWritten = 0;
                return false;
            }
 
            GetHashAndResetCore(destination.Slice(0, HashLengthInBytes));
            bytesWritten = HashLengthInBytes;
            return true;
        }
 
        /// <summary>
        ///   Writes the computed hash value to <paramref name="destination"/>
        ///   then clears the accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <returns>
        ///   The number of bytes written to <paramref name="destination"/>,
        ///   which is always <see cref="HashLengthInBytes"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        ///   <paramref name="destination"/> is shorter than <see cref="HashLengthInBytes"/>.
        /// </exception>
        public int GetHashAndReset(Span<byte> destination)
        {
            if (destination.Length < HashLengthInBytes)
            {
                ThrowDestinationTooShort();
            }
 
            GetHashAndResetCore(destination.Slice(0, HashLengthInBytes));
            return HashLengthInBytes;
        }
 
        /// <summary>
        ///   Writes the computed hash value to <paramref name="destination"/>
        ///   then clears the accumulated state.
        /// </summary>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <remarks>
        ///   <para>
        ///     Implementations of this method must write exactly
        ///     <see cref="HashLengthInBytes"/> bytes to <paramref name="destination"/>.
        ///     Do not assume that the buffer was zero-initialized.
        ///   </para>
        ///   <para>
        ///     The <see cref="NonCryptographicHashAlgorithm"/> class validates the
        ///     size of the buffer before calling this method, and slices the span
        ///     down to be exactly <see cref="HashLengthInBytes"/> in length.
        ///   </para>
        ///   <para>
        ///     The default implementation of this method calls
        ///     <see cref="GetCurrentHashCore"/> followed by <see cref="Reset"/>.
        ///     Overrides of this method do not need to call either of those methods,
        ///     but must ensure that the caller cannot observe a difference in behavior.
        ///   </para>
        /// </remarks>
        protected virtual void GetHashAndResetCore(Span<byte> destination)
        {
            Debug.Assert(destination.Length == HashLengthInBytes);
 
            GetCurrentHashCore(destination);
            Reset();
        }
 
        /// <summary>
        ///   This method is not supported and should not be called.
        ///   Call <see cref="GetCurrentHash()"/> or <see cref="GetHashAndReset()"/>
        ///   instead.
        /// </summary>
        /// <returns>This method will always throw a <see cref="NotSupportedException"/>.</returns>
        /// <exception cref="NotSupportedException">In all cases.</exception>
        [EditorBrowsable(EditorBrowsableState.Never)]
        [Obsolete("Use GetCurrentHash() to retrieve the computed hash code.", true)]
#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
        public override int GetHashCode()
#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
        {
            throw new NotSupportedException("GetHashCode not supported");
        }
 
        [DoesNotReturn]
        private protected static void ThrowDestinationTooShort() =>
            throw new ArgumentException("Destination is too short", "destination");
    }
}