-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCopyToHashCalculator.cs
More file actions
177 lines (160 loc) · 7.27 KB
/
CopyToHashCalculator.cs
File metadata and controls
177 lines (160 loc) · 7.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Light.GuardClauses;
namespace Light.TemporaryStreams.Hashing;
/// <summary>
/// Represents a hash calculator that can be used when copying a stream to a temporary stream. This class is not
/// thread-safe.
/// </summary>
public sealed class CopyToHashCalculator : IAsyncDisposable
{
private CryptoStream? _cryptoStream;
private string? _hash;
private byte[]? _hashArray;
/// <summary>
/// Initializes a new instance of <see cref="CopyToHashCalculator" />.
/// </summary>
/// <param name="hashAlgorithm">The hash algorithm to use.</param>
/// <param name="conversionMethod">The enum value identifying how hash byte arrays are converted to strings.</param>
/// <param name="name">A name that uniquely identifies the hash algorithm.</param>
public CopyToHashCalculator(HashAlgorithm hashAlgorithm, HashConversionMethod conversionMethod, string? name = null)
{
HashAlgorithm = hashAlgorithm.MustNotBeNull();
ConversionMethod = conversionMethod.MustBeValidEnumValue();
Name = name ?? DetermineDefaultName(hashAlgorithm);
}
/// <summary>
/// Gets the type of conversion method that is applied to obtain a string representation from the hash byte array.
/// </summary>
public HashConversionMethod ConversionMethod { get; }
/// <summary>
/// Gets the hash algorithm that is used to calculate the hash.
/// </summary>
public HashAlgorithm HashAlgorithm { get; }
/// <summary>
/// The name that uniquely identifies this calculator instance within the scope of a CopyTo operation.
/// </summary>
public string Name { get; }
/// <summary>
/// The calculated hash in string representation.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when <see cref="ObtainHashFromAlgorithm" /> has not been called yet.
/// </exception>
public string Hash
{
get
{
var hash = _hash;
if (hash is null)
{
throw new InvalidOperationException(
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(Hash)} property"
);
}
return hash;
}
}
/// <summary>
/// The calculated hash in byte array representation.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when <see cref="ObtainHashFromAlgorithm" /> has not been called yet.
/// </exception>
public byte[] HashArray
{
get
{
var hashArray = _hashArray;
if (hashArray is null)
{
throw new InvalidOperationException(
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(HashArray)} property"
);
}
return hashArray;
}
}
/// <summary>
/// Asynchronously disposes the resources used by the current instance, including the CryptoStream and the hash algorithm.
/// Ensures proper cleanup of unmanaged resources and releases memory associated with the instance.
/// </summary>
/// <returns>A ValueTask that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync()
{
if (_cryptoStream is not null)
{
await _cryptoStream.DisposeAsync();
_cryptoStream = null;
}
HashAlgorithm.Dispose();
}
private static string DetermineDefaultName(HashAlgorithm hashAlgorithm)
{
/* Some of the .NET hash algorithms (like .SHA1, MD5) are actually just abstract base classes. They have a
* nested type called implementation which can be instantiated, but this type is not publicly visible. GetType()
* will likely return the implementation type, which is not what we want as callers would want the name of the
* base type instead. This is why we check the name of the type for ".Implementation" and if found, we return
* the name of the base type instead.
*
* Other types like HMACSHA256 are public non-abstract types and can be used as expected. */
var type = hashAlgorithm.GetType();
var name = type.Name;
var baseTypeName = type.BaseType?.Name;
return name.Equals("Implementation", StringComparison.Ordinal) && !baseTypeName.IsNullOrWhiteSpace() ?
baseTypeName :
name;
}
/// <summary>
/// Creates a CryptoStream wrapped around the specified stream. The CryptoStream
/// is configured to calculate a hash using the hash algorithm provided by the
/// implementation of the derived class. This method can only be called once
/// for the lifetime of this instance; later calls will throw an <see cref="InvalidOperationException" />.
/// </summary>
/// <param name="wrappedStream">The stream to be wrapped by the CryptoStream.</param>
/// <param name="leaveWrappedStreamOpen">
/// The value indicating whether the wrapped stream should not be disposed when the Crypt Stream created by this method is disposed.
/// </param>
/// <returns>The Crypto Stream configured to calculate the hash on write operations.</returns>
/// <exception cref="InvalidOperationException">Thrown when this method is called more than once.</exception>
public CryptoStream CreateWrappingCryptoStream(Stream wrappedStream, bool leaveWrappedStreamOpen)
{
return _cryptoStream = new CryptoStream(
wrappedStream,
HashAlgorithm,
CryptoStreamMode.Write,
leaveWrappedStreamOpen
);
}
/// <summary>
/// Finalizes the hash calculation and retrieves the hash from the configured hash algorithm.
/// The resulting hash is stored in Base64 representation in the <see cref="Hash" /> property.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the hash algorithm has not been initialized by calling <see cref="CreateWrappingCryptoStream" /> beforehand
/// or if no data has been written to the underlying crypto stream, resulting in no hash calculation.
/// </exception>
public void ObtainHashFromAlgorithm()
{
_hashArray =
HashAlgorithm
.Hash
.MustNotBeNull(
() => new InvalidOperationException(
"The crypto stream was not written to - no hash was calculated"
)
);
_hash = HashConverter.ConvertHashToString(_hashArray, ConversionMethod);
}
/// <summary>
/// Converts a <see cref="HashAlgorithm" /> to a <see cref="CopyToHashCalculator" /> using the default settings
/// (hash array is converted to a Base64 string, name is identical to the type name).
/// </summary>
/// <param name="hashAlgorithm">The hash algorithm that should be used to calculate the hash.</param>
/// <returns>A new instance of <see cref="CopyToHashCalculator" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hashAlgorithm" /> is null.</exception>
public static implicit operator CopyToHashCalculator(HashAlgorithm hashAlgorithm) =>
new (hashAlgorithm, HashConversionMethod.Base64);
}