sdk/Common/Internal/HashStream.cs (183 lines of code) (raw):

/* * Copyright (C) Alibaba Cloud Computing * All rights reserved. * */ using System; using System.IO; namespace Aliyun.OSS.Common.Internal { /// <summary> /// A wrapper stream that calculates a hash of the base stream as it /// is being read. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an ClientException. /// If CalculatedHash is calculated for only the portion of the stream that /// is read. /// </summary> /// <exception cref="Aliyun.OSS.Common.ClientException"> /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// </exception> public abstract class HashStream : WrapperStream { #region Properties /// <summary> /// Algorithm to use to calculate hash. /// </summary> protected IHashingWrapper Algorithm { get; set; } /// <summary> /// True if hashing is finished and no more hashing should be done; /// otherwise false. /// </summary> protected bool FinishedHashing { get { return CalculatedHash != null; } } /// <summary> /// Current position in the stream. /// </summary> protected long CurrentPosition { get; private set; } /// <summary> /// Calculated hash for the stream. /// This value is set only after the stream is closed. /// </summary> public byte[] CalculatedHash { get; protected set; } /// <summary> /// Expected hash value. Compared against CalculatedHash upon Close(). /// If the hashes are different, an ClientException is thrown. /// </summary> public byte[] ExpectedHash { get; private set; } /// <summary> /// Expected length of stream. /// </summary> public long ExpectedLength { get; protected set; } #endregion #region Constructors /// <summary> /// Initializes an HashStream with a hash algorithm and a base stream. /// </summary> /// <param name="baseStream">Stream to calculate hash for.</param> protected HashStream(Stream baseStream, long expectedLength) : this(baseStream, null, expectedLength) { } /// <summary> /// Initializes an HashStream with a hash algorithm and a base stream. /// </summary> /// <param name="baseStream">Stream to calculate hash for.</param> /// <param name="expectedHash"> /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// </param> /// <param name="expectedLength"> /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// </param> protected HashStream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream) { ExpectedHash = expectedHash; ExpectedLength = expectedLength; ValidateBaseStream(); Reset(); } #endregion #region Stream overrides /// <summary> /// Reads a sequence of bytes from the current stream and advances the position /// within the stream by the number of bytes read. /// </summary> /// <param name="buffer"> /// An array of bytes. When this method returns, the buffer contains the specified /// byte array with the values between offset and (offset + count - 1) replaced /// by the bytes read from the current source. /// </param> /// <param name="offset"> /// The zero-based byte offset in buffer at which to begin storing the data read /// from the current stream. /// </param> /// <param name="count"> /// The maximum number of bytes to be read from the current stream. /// </param> /// <returns> /// The total number of bytes read into the buffer. This can be less than the /// number of bytes requested if that many bytes are not currently available, /// or zero (0) if the end of the stream has been reached. /// </returns> public override int Read(byte[] buffer, int offset, int count) { int result = base.Read(buffer, offset, count); CurrentPosition += result; if (!FinishedHashing) { Algorithm.AppendBlock(buffer, offset, result); } return result; } /// <summary> /// Write the specified buffer, offset and count. /// </summary> /// <returns>The write.</returns> /// <param name="buffer">Buffer.</param> /// <param name="offset">Offset.</param> /// <param name="count">Count.</param> public override void Write(byte[] buffer, int offset, int count) { base.Write(buffer, offset, count); CurrentPosition += count; if (!FinishedHashing) { Algorithm.AppendBlock(buffer, offset, count); } } #if !PCL && !CORECLR /// <summary> /// Closes the underlying stream and finishes calculating the hash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// this method will throw an ClientException. /// </summary> /// <exception cref="Aliyun.OSS.Common.ClientException"> /// If ExpectedHash is set and is different from CalculateHash that the stream calculates. /// </exception> public override void Close() { CalculateHash(); base.Close(); } #endif protected override void Dispose(bool disposing) { try { CalculateHash(); if (disposing && Algorithm != null) { Algorithm.Dispose(); Algorithm = null; } } finally { base.Dispose(disposing); } } /// <summary> /// Gets a value indicating whether the current stream supports seeking. /// HashStream does not support seeking, this will always be false. /// </summary> public override bool CanSeek { get { // Restrict random access, as this will break hashing. return false; } } /// <summary> /// Gets or sets the position within the current stream. /// HashStream does not support seeking, attempting to set Position /// will throw NotSupportedException. /// </summary> public override long Position { set { // Restrict random access, as this will break hashing. throw new NotSupportedException("HashStream does not support seeking"); } } /// <summary> /// Sets the position within the current stream. /// HashStream does not support seeking, attempting to call Seek /// will throw NotSupportedException. /// </summary> /// <param name="offset">A byte offset relative to the origin parameter.</param> /// <param name="origin"> /// A value of type System.IO.SeekOrigin indicating the reference point used /// to obtain the new position.</param> /// <returns>The new position within the current stream.</returns> public override long Seek(long offset, SeekOrigin origin) { // Restrict random access, as this will break hashing. throw new NotSupportedException("HashStream does not support seeking"); } /// <summary> /// Gets the overridden length used to construct the HashStream /// </summary> public override long Length { get { return this.ExpectedLength; } } #endregion #region Public methods /// <summary> /// Calculates the hash for the stream so far and disables any further /// hashing. /// </summary> public virtual void CalculateHash() { if (!FinishedHashing) { if (ExpectedLength < 0 || CurrentPosition == ExpectedLength) { CalculatedHash = Algorithm.AppendLastBlock(new byte[0]); } else CalculatedHash = new byte[0]; if (CalculatedHash.Length > 0 && ExpectedHash != null && ExpectedHash.Length > 0) { if (!CompareHashes(ExpectedHash, CalculatedHash)) throw new ClientException("Expected hash not equal to calculated hash"); } } } /// <summary> /// Resets the hash stream to starting state. /// Use this if the underlying stream has been modified and needs /// to be rehashed without reconstructing the hierarchy. /// </summary> public void Reset() { CurrentPosition = 0; CalculatedHash = null; if (Algorithm != null) Algorithm.Clear(); var baseHashStream = BaseStream as HashStream; if (baseHashStream != null) { baseHashStream.Reset(); } } #endregion #region Private methods /// <summary> /// Validates the underlying stream. /// </summary> private void ValidateBaseStream() { // Fast-fail on unusable streams if (!BaseStream.CanRead && !BaseStream.CanWrite) throw new InvalidDataException("HashStream does not support base streams that are not capable of reading or writing"); } /// <summary> /// Compares two hashes (arrays of bytes). /// </summary> /// <param name="expected">Expected hash.</param> /// <param name="actual">Actual hash.</param> /// <returns> /// True if the hashes are identical; otherwise false. /// </returns> protected static bool CompareHashes(byte[] expected, byte[] actual) { if (ReferenceEquals(expected, actual)) return true; if (expected == null || actual == null) return (expected == actual); if (expected.Length != actual.Length) return false; for (int i = 0; i < expected.Length; i++) { if (expected[i] != actual[i]) return false; } return true; } #endregion } /// <summary> /// A wrapper stream that calculates a hash of the base stream as it /// is being read or written. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an ClientException. /// If base stream's position is not 0 or HashOnReads is true and the entire stream is /// not read, the CalculatedHash will be set to an empty byte array and /// comparison to ExpectedHash will not be made. /// </summary> /// <exception cref="Aliyun.OSS.Common.ClientException"> /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// </exception> public class HashStream<T> : HashStream where T : IHashingWrapper, new() { #region Constructors /// <summary> /// Initializes an HashStream with a hash algorithm and a base stream. /// </summary> /// <param name="baseStream">Stream to calculate hash for.</param> /// <param name="expectedHash"> /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// </param> /// <param name="expectedLength"> /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// </param> public HashStream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream, expectedHash, expectedLength) { Algorithm = new T(); } #endregion } /// <summary> /// A wrapper stream that calculates an MD5 hash of the base stream as it /// is being read or written. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an ClientException. /// If base stream's position is not 0 or HashOnReads is true and the entire stream is /// not read, the CalculatedHash will be set to an empty byte array and /// comparison to ExpectedHash will not be made. /// </summary> /// <exception cref="Aliyun.OSS.Common.ClientException"> /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// </exception> public class MD5Stream : HashStream<HashingWrapperMD5> { #region Constructors /// <summary> /// Initializes an MD5Stream with a base stream. /// </summary> /// <param name="baseStream">Stream to calculate hash for.</param> /// <param name="expectedHash"> /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// </param> /// <param name="expectedLength"> /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// </param> public MD5Stream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream, expectedHash, expectedLength) { } #endregion } public class Crc64Stream : HashStream<HashingWrapperCrc64> { public Crc64Stream(Stream baseStream, byte[] expectedHash, long expectedLength, ulong initCrc64 = 0) : base(baseStream, expectedHash, expectedLength) { if (!(this.Algorithm is HashingWrapperCrc64)) { throw new ClientException("Crc64Stream's Algorithm must be HashingWrapperCrc64"); } HashingWrapperCrc64 crcWrapper = this.Algorithm as HashingWrapperCrc64; crcWrapper.SetInitCrc64(initCrc64); } } }