Skip to content

Commit a9f40c4

Browse files
authored
Merge pull request #7 from feO2x/copilot/fix-6
Add comprehensive tests for CopyToTemporaryStreamAsync overloads and fix plugin bugs
2 parents ef104da + 814ecb0 commit a9f40c4

File tree

10 files changed

+790
-47
lines changed

10 files changed

+790
-47
lines changed

Light.TemporaryStreams.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,5 @@
163163
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
164164
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
165165
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
166+
<s:Boolean x:Key="/Default/UserDictionary/Words/=hmacsha/@EntryIndexedValue">True</s:Boolean>
166167
</wpf:ResourceDictionary>

src/Light.TemporaryStreams.Core/Hashing/CopyToHashCalculator.cs

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,11 @@ public sealed class CopyToHashCalculator : IAsyncDisposable
2222
/// <param name="hashAlgorithm">The hash algorithm to use.</param>
2323
/// <param name="conversionMethod">The enum value identifying how hash byte arrays are converted to strings.</param>
2424
/// <param name="name">A name that uniquely identifies the hash algorithm.</param>
25-
public CopyToHashCalculator(
26-
HashAlgorithm hashAlgorithm,
27-
HashConversionMethod conversionMethod,
28-
string? name = null
29-
)
25+
public CopyToHashCalculator(HashAlgorithm hashAlgorithm, HashConversionMethod conversionMethod, string? name = null)
3026
{
3127
HashAlgorithm = hashAlgorithm.MustNotBeNull();
3228
ConversionMethod = conversionMethod.MustBeValidEnumValue();
33-
Name = name ?? hashAlgorithm.GetType().Name;
29+
Name = name ?? DetermineDefaultName(hashAlgorithm);
3430
}
3531

3632
/// <summary>
@@ -54,25 +50,43 @@ public CopyToHashCalculator(
5450
/// <exception cref="InvalidOperationException">
5551
/// Thrown when <see cref="ObtainHashFromAlgorithm" /> has not been called yet.
5652
/// </exception>
57-
public string Hash =>
58-
_hash.MustNotBeNull(
59-
() => new InvalidOperationException(
60-
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(Hash)} property."
61-
)
62-
);
53+
public string Hash
54+
{
55+
get
56+
{
57+
var hash = _hash;
58+
if (hash is null)
59+
{
60+
throw new InvalidOperationException(
61+
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(Hash)} property"
62+
);
63+
}
64+
65+
return hash;
66+
}
67+
}
6368

6469
/// <summary>
6570
/// The calculated hash in byte array representation.
6671
/// </summary>
6772
/// <exception cref="InvalidOperationException">
6873
/// Thrown when <see cref="ObtainHashFromAlgorithm" /> has not been called yet.
6974
/// </exception>
70-
public byte[] HashArray =>
71-
_hashArray.MustNotBeNull(
72-
() => new InvalidOperationException(
73-
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(HashArray)} property."
74-
)
75-
);
75+
public byte[] HashArray
76+
{
77+
get
78+
{
79+
var hashArray = _hashArray;
80+
if (hashArray is null)
81+
{
82+
throw new InvalidOperationException(
83+
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(HashArray)} property"
84+
);
85+
}
86+
87+
return hashArray;
88+
}
89+
}
7690

7791
/// <summary>
7892
/// Asynchronously disposes the resources used by the current instance, including the CryptoStream and the hash algorithm.
@@ -90,6 +104,23 @@ public async ValueTask DisposeAsync()
90104
HashAlgorithm.Dispose();
91105
}
92106

107+
private static string DetermineDefaultName(HashAlgorithm hashAlgorithm)
108+
{
109+
/* Some of the .NET hash algorithms (like .SHA1, MD5) are actually just abstract base classes. They have a
110+
* nested type called implementation which can be instantiated, but this type is not publicly visible. GetType()
111+
* will likely return the implementation type, which is not what we want as callers would want the name of the
112+
* base type instead. This is why we check the name of the type for ".Implementation" and if found, we return
113+
* the name of the base type instead.
114+
*
115+
* Other types like HMACSHA256 are public non-abstract types and can be used as expected. */
116+
var type = hashAlgorithm.GetType();
117+
var name = type.Name;
118+
var baseTypeName = type.BaseType?.Name;
119+
return name.Equals("Implementation", StringComparison.Ordinal) && !baseTypeName.IsNullOrWhiteSpace() ?
120+
baseTypeName :
121+
name;
122+
}
123+
93124
/// <summary>
94125
/// Creates a CryptoStream wrapped around the specified stream. The CryptoStream
95126
/// is configured to calculate a hash using the hash algorithm provided by the
@@ -127,22 +158,13 @@ public void ObtainHashFromAlgorithm()
127158
.Hash
128159
.MustNotBeNull(
129160
() => new InvalidOperationException(
130-
"The crypto stream was not written to - no hash was calculated."
161+
"The crypto stream was not written to - no hash was calculated"
131162
)
132163
);
133164

134-
_hash = ConvertHashToString(_hashArray);
165+
_hash = HashConverter.ConvertHashToString(_hashArray, ConversionMethod);
135166
}
136167

137-
private string ConvertHashToString(byte[] hashArray) =>
138-
ConversionMethod switch
139-
{
140-
HashConversionMethod.Base64 => Convert.ToBase64String(hashArray),
141-
HashConversionMethod.UpperHexadecimal => Convert.ToHexString(hashArray),
142-
HashConversionMethod.None => "",
143-
_ => throw new InvalidDataException($"{nameof(ConversionMethod)} has an invalid value")
144-
};
145-
146168
/// <summary>
147169
/// Converts a <see cref="HashAlgorithm" /> to a <see cref="CopyToHashCalculator" /> using the default settings
148170
/// (hash array is converted to a Base64 string, name is identical to the type name).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Light.TemporaryStreams.Hashing;
5+
6+
/// <summary>
7+
/// Converts hash byte arrays to strings.
8+
/// </summary>
9+
public static class HashConverter
10+
{
11+
/// <summary>
12+
/// Converts the hash byte array to a string based on the specified <see cref="HashConversionMethod" />.
13+
/// </summary>
14+
/// <param name="hashArray">The hash byte array to convert.</param>
15+
/// <param name="conversionMethod">The enum value specifying how the hash byte array should be converted to a string.</param>
16+
/// <returns>The hash as a string.</returns>
17+
/// <exception cref="InvalidDataException">
18+
/// Thrown if <paramref name="conversionMethod" /> has an invalid value.
19+
/// </exception>
20+
public static string ConvertHashToString(byte[] hashArray, HashConversionMethod conversionMethod) =>
21+
conversionMethod switch
22+
{
23+
HashConversionMethod.Base64 => Convert.ToBase64String(hashArray),
24+
HashConversionMethod.UpperHexadecimal => Convert.ToHexString(hashArray),
25+
HashConversionMethod.None => "",
26+
_ => throw new ArgumentOutOfRangeException(
27+
nameof(conversionMethod),
28+
$"{nameof(conversionMethod)} has an invalid value '{conversionMethod}'"
29+
)
30+
};
31+
}

src/Light.TemporaryStreams.Core/Hashing/HashingPlugin.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,18 @@ public async ValueTask DisposeAsync()
7474
/// <param name="innerStream">The inner stream to be wrapped by the hash calculators.</param>
7575
/// <param name="cancellationToken">The optional token to cancel the asynchronous operation.</param>
7676
/// <returns>The outermost CryptoStream.</returns>
77+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="innerStream" /> is null.</exception>
7778
public ValueTask<Stream> SetUpAsync(Stream innerStream, CancellationToken cancellationToken = default)
7879
{
7980
innerStream.MustNotBeNull();
80-
CryptoStream outermostStream = null!; // In the constructor, we ensure that the immutable array is not empty
81+
Stream currentStream = innerStream;
8182
for (var i = 0; i < HashCalculators.Length; i++)
8283
{
83-
outermostStream =
84-
HashCalculators[i].CreateWrappingCryptoStream(innerStream, leaveWrappedStreamOpen: true);
84+
currentStream = HashCalculators[i].CreateWrappingCryptoStream(currentStream, leaveWrappedStreamOpen: true);
8585
}
8686

87-
_outermostCryptoStream = outermostStream;
88-
return new ValueTask<Stream>(outermostStream);
87+
_outermostCryptoStream = (CryptoStream)currentStream;
88+
return new ValueTask<Stream>(currentStream);
8989
}
9090

9191
/// <summary>

src/Light.TemporaryStreams.Core/TemporaryStreamServiceExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static class TemporaryStreamServiceExtensions
3838
/// <exception cref="ArgumentNullException">Thrown when <paramref name="temporaryStreamService" /> or <paramref name="source" /> are null.</exception>
3939
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="copyBufferSize" /> has a value that is less than 0.</exception>
4040
public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
41-
this TemporaryStreamService temporaryStreamService,
41+
this ITemporaryStreamService temporaryStreamService,
4242
Stream source,
4343
string? filePath = null,
4444
TemporaryStreamServiceOptions? options = null,
@@ -100,7 +100,7 @@ await source
100100
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="copyBufferSize" /> has a value that is less than 0.</exception>
101101
/// <exception cref="EmptyCollectionException">Thrown when <paramref name="plugins" /> is empty or the default instance.</exception>
102102
public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
103-
this TemporaryStreamService temporaryStreamService,
103+
this ITemporaryStreamService temporaryStreamService,
104104
Stream source,
105105
ImmutableArray<ICopyToTemporaryStreamPlugin> plugins,
106106
string? filePath = null,
@@ -123,7 +123,7 @@ public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
123123
{
124124
for (var i = 0; i < plugins.Length; i++)
125125
{
126-
outermostStream = await plugins[i].SetUpAsync(source, cancellationToken).ConfigureAwait(false);
126+
outermostStream = await plugins[i].SetUpAsync(outermostStream, cancellationToken).ConfigureAwait(false);
127127
}
128128

129129
if (copyBufferSize.HasValue)

0 commit comments

Comments
 (0)