/*******************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
* this file except in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file.
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* *****************************************************************************
* __ _ _ ___
* ( )( \/\/ )/ __)
* /__\ \ / \__ \
* (_)(_) \/\/ (___/
*
* AWS SDK for .NET
* API Version: 2006-03-01
*
*/
using Amazon.Util.Internal;
using System;
using System.IO;
#if AWS_ASYNC_API
using System.Threading;
using System.Threading.Tasks;
#endif
namespace Amazon.Runtime.Internal.Util
{
///
/// 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 AmazonClientException.
/// If CalculatedHash is calculated for only the portion of the stream that
/// is read.
///
///
/// 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.
///
public abstract class HashStream : WrapperStream
{
#region Properties
///
/// Algorithm to use to calculate hash.
///
protected IHashingWrapper Algorithm { get; set; }
///
/// True if hashing is finished and no more hashing should be done;
/// otherwise false.
///
protected bool FinishedHashing { get { return CalculatedHash != null; } }
///
/// Current position in the stream.
///
protected long CurrentPosition { get; private set; }
///
/// Calculated hash for the stream.
/// This value is set only after the stream is closed.
///
public byte[] CalculatedHash { get; protected set; }
///
/// Expected hash value. Compared against CalculatedHash upon Close().
/// If the hashes are different, an AmazonClientException is thrown.
///
public byte[] ExpectedHash { get; private set; }
///
/// Expected length of stream.
///
public long ExpectedLength { get; protected set; }
#endregion
#region Constructors
/////
///// Initializes an HashStream with a hash algorithm and a base stream.
/////
///// Stream to calculate hash for.
//protected HashStream(Stream baseStream)
// : this(baseStream, null) { }
///
/// Initializes an HashStream with a hash algorithm and a base stream.
///
/// Stream to calculate hash for.
///
/// Expected hash. Will be compared against calculated hash on stream close.
/// Pass in null to disable check.
///
///
/// Expected length of the stream. If the reading stops before reaching this
/// position, CalculatedHash will be set to empty array.
///
protected HashStream(Stream baseStream, byte[] expectedHash, long expectedLength)
: base(baseStream)
{
ExpectedHash = expectedHash;
ExpectedLength = expectedLength;
ValidateBaseStream();
Reset();
}
#endregion
#region Stream overrides
///
/// Reads a sequence of bytes from the current stream and advances the position
/// within the stream by the number of bytes read.
///
///
/// 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.
///
///
/// The zero-based byte offset in buffer at which to begin storing the data read
/// from the current stream.
///
///
/// The maximum number of bytes to be read from the current stream.
///
///
/// 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.
///
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);
}
// Calculate the hash if this was the final read
if (result == 0)
{
CalculateHash();
}
return result;
}
#if AWS_ASYNC_API
///
/// Asynchronously reads a sequence of bytes from the current stream, advances
/// the position within the stream by the number of bytes read, and monitors
/// cancellation requests.
///
///
/// 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.
///
///
/// The zero-based byte offset in buffer at which to begin storing the data read
/// from the current stream.
///
///
/// The maximum number of bytes to be read from the current stream.
///
///
/// The token to monitor for cancellation requests. The default value is
/// System.Threading.CancellationToken.None.
///
///
/// A task that represents the asynchronous read operation. The value of the TResult
/// parameter contains 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.
///
public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
int result = await base.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
CurrentPosition += result;
if (!FinishedHashing)
{
Algorithm.AppendBlock(buffer, offset, result);
}
// Calculate the hash if this was the final read
if (result == 0)
{
CalculateHash();
}
return result;
}
#endif
#if !NETSTANDARD
///
/// 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 AmazonClientException.
///
///
/// If ExpectedHash is set and is different from CalculateHash that the stream calculates.
///
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);
}
}
///
/// Gets a value indicating whether the current stream supports seeking.
/// HashStream does not support seeking, this will always be false.
///
public override bool CanSeek
{
get
{
// Restrict random access, as this will break hashing.
return false;
}
}
///
/// Gets or sets the position within the current stream.
/// HashStream does not support seeking, attempting to set Position
/// will throw NotSupportedException.
///
public override long Position
{
get
{
throw new NotSupportedException("HashStream does not support seeking");
}
set
{
// Restrict random access, as this will break hashing.
throw new NotSupportedException("HashStream does not support seeking");
}
}
///
/// Sets the position within the current stream.
/// HashStream does not support seeking, attempting to call Seek
/// will throw NotSupportedException.
///
/// A byte offset relative to the origin parameter.
///
/// A value of type System.IO.SeekOrigin indicating the reference point used
/// to obtain the new position.
/// The new position within the current stream.
public override long Seek(long offset, SeekOrigin origin)
{
// Restrict random access, as this will break hashing.
throw new NotSupportedException("HashStream does not support seeking");
}
///
/// Gets the overridden length used to construct the HashStream
///
public override long Length
{
get
{
return this.ExpectedLength;
}
}
#endregion
#region Public methods
///
/// Calculates the hash for the stream so far and disables any further
/// hashing.
///
public virtual void CalculateHash()
{
if (!FinishedHashing)
{
if (ExpectedLength < 0 || CurrentPosition == ExpectedLength)
{
CalculatedHash = Algorithm.AppendLastBlock(ArrayEx.Empty());
}
else
CalculatedHash = ArrayEx.Empty();
if (CalculatedHash.Length > 0 && ExpectedHash != null && ExpectedHash.Length > 0)
{
if (!CompareHashes(ExpectedHash, CalculatedHash))
throw new AmazonClientException("Expected hash not equal to calculated hash");
}
}
}
///
/// 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.
///
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
///
/// Validates the underlying stream.
///
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");
}
///
/// Compares two hashes (arrays of bytes).
///
/// Expected hash.
/// Actual hash.
///
/// True if the hashes are identical; otherwise false.
///
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
}
///
/// 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 AmazonClientException.
/// 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.
///
///
/// 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.
///
public class HashStream : HashStream
where T : IHashingWrapper, new()
{
#region Constructors
///
/// Initializes an HashStream with a hash algorithm and a base stream.
///
/// Stream to calculate hash for.
///
/// Expected hash. Will be compared against calculated hash on stream close.
/// Pass in null to disable check.
///
///
/// Expected length of the stream. If the reading stops before reaching this
/// position, CalculatedHash will be set to empty array.
///
public HashStream(Stream baseStream, byte[] expectedHash, long expectedLength)
: base(baseStream, expectedHash, expectedLength)
{
Algorithm = new T();
}
#endregion
}
///
/// 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 AmazonClientException.
/// 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.
///
///
/// 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.
///
public class MD5Stream : HashStream
{
private Logger _logger;
#region Constructors
///
/// Initializes an MD5Stream with a base stream.
///
/// Stream to calculate hash for.
///
/// Expected hash. Will be compared against calculated hash on stream close.
/// Pass in null to disable check.
///
///
/// Expected length of the stream. If the reading stops before reaching this
/// position, CalculatedHash will be set to empty array.
///
public MD5Stream(Stream baseStream, byte[] expectedHash, long expectedLength)
: base(baseStream, expectedHash, expectedLength)
{
_logger = Logger.GetLogger(this.GetType());
}
#endregion
}
}