File: System\IO\Hashing\NonCryptographicHashAlgorithm.cs
Web Access
Project: src\src\libraries\System.IO.Hashing\src\System.IO.Hashing.csproj (System.IO.Hashing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.IO.Hashing
{
    /// <summary>
    ///   Represents a non-cryptographic hash algorithm.
    /// </summary>
    public 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));
            }
 
            stream.CopyTo(new CopyToDestinationStream(this));
        }
 
        /// <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 stream.CopyToAsync(
                new CopyToDestinationStream(this),
#if !NET
                81_920, // default size used by Stream.CopyTo{Async}
#endif
                cancellationToken);
        }
 
        /// <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(SR.NotSupported_GetHashCode);
        }
 
        [DoesNotReturn]
        private protected static void ThrowDestinationTooShort() =>
            throw new ArgumentException(SR.Argument_DestinationTooShort, "destination");
 
        /// <summary>Stream-derived type used to support copying from a source stream to this instance via CopyTo{Async}.</summary>
        private sealed class CopyToDestinationStream(NonCryptographicHashAlgorithm hash) : Stream
        {
            public override bool CanWrite => true;
 
            public override void Write(byte[] buffer, int offset, int count) => hash.Append(buffer.AsSpan(offset, count));
 
            public override void WriteByte(byte value) =>
                hash.Append(
#if NET
                    new ReadOnlySpan<byte>(in value)
#else
                    [value]
#endif
                    );
 
            public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
            {
                hash.Append(buffer.AsSpan(offset, count));
                return Task.CompletedTask;
            }
 
#if NET
            public override void Write(ReadOnlySpan<byte> buffer) => hash.Append(buffer);
 
            public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
            {
                hash.Append(buffer.Span);
                return default;
            }
 
            public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
                TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count), callback, state);
 
            public override void EndWrite(IAsyncResult asyncResult) =>
                TaskToAsyncResult.End(asyncResult);
#endif
 
            public override void Flush() { }
 
            public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 
            public override bool CanRead => false;
 
            public override bool CanSeek => false;
 
            public override long Length => throw new NotSupportedException();
 
            public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
 
            public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
 
            public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
 
            public override void SetLength(long value) => throw new NotSupportedException();
        }
    }
}