Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Light.TemporaryStreams.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hmacsha/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
80 changes: 51 additions & 29 deletions src/Light.TemporaryStreams.Core/Hashing/CopyToHashCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,11 @@ public sealed class CopyToHashCalculator : IAsyncDisposable
/// <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
)
public CopyToHashCalculator(HashAlgorithm hashAlgorithm, HashConversionMethod conversionMethod, string? name = null)
{
HashAlgorithm = hashAlgorithm.MustNotBeNull();
ConversionMethod = conversionMethod.MustBeValidEnumValue();
Name = name ?? hashAlgorithm.GetType().Name;
Name = name ?? DetermineDefaultName(hashAlgorithm);
}

/// <summary>
Expand All @@ -54,25 +50,43 @@ public CopyToHashCalculator(
/// <exception cref="InvalidOperationException">
/// Thrown when <see cref="ObtainHashFromAlgorithm" /> has not been called yet.
/// </exception>
public string Hash =>
_hash.MustNotBeNull(
() => new InvalidOperationException(
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(Hash)} property."
)
);
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 =>
_hashArray.MustNotBeNull(
() => new InvalidOperationException(
$"ObtainHashFromAlgorithm must be called before accessing the {nameof(HashArray)} property."
)
);
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.
Expand All @@ -90,6 +104,23 @@ public async ValueTask DisposeAsync()
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
Expand Down Expand Up @@ -127,22 +158,13 @@ public void ObtainHashFromAlgorithm()
.Hash
.MustNotBeNull(
() => new InvalidOperationException(
"The crypto stream was not written to - no hash was calculated."
"The crypto stream was not written to - no hash was calculated"
)
);

_hash = ConvertHashToString(_hashArray);
_hash = HashConverter.ConvertHashToString(_hashArray, ConversionMethod);
}

private string ConvertHashToString(byte[] hashArray) =>
ConversionMethod switch
{
HashConversionMethod.Base64 => Convert.ToBase64String(hashArray),
HashConversionMethod.UpperHexadecimal => Convert.ToHexString(hashArray),
HashConversionMethod.None => "",
_ => throw new InvalidDataException($"{nameof(ConversionMethod)} has an invalid value")
};

/// <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).
Expand Down
31 changes: 31 additions & 0 deletions src/Light.TemporaryStreams.Core/Hashing/HashConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
ο»Ώusing System;
using System.IO;

namespace Light.TemporaryStreams.Hashing;

/// <summary>
/// Converts hash byte arrays to strings.
/// </summary>
public static class HashConverter
{
/// <summary>
/// Converts the hash byte array to a string based on the specified <see cref="HashConversionMethod" />.
/// </summary>
/// <param name="hashArray">The hash byte array to convert.</param>
/// <param name="conversionMethod">The enum value specifying how the hash byte array should be converted to a string.</param>
/// <returns>The hash as a string.</returns>
/// <exception cref="InvalidDataException">
/// Thrown if <paramref name="conversionMethod" /> has an invalid value.
/// </exception>
public static string ConvertHashToString(byte[] hashArray, HashConversionMethod conversionMethod) =>
conversionMethod switch
{
HashConversionMethod.Base64 => Convert.ToBase64String(hashArray),
HashConversionMethod.UpperHexadecimal => Convert.ToHexString(hashArray),
HashConversionMethod.None => "",
_ => throw new ArgumentOutOfRangeException(
nameof(conversionMethod),
$"{nameof(conversionMethod)} has an invalid value '{conversionMethod}'"
)
};
}
10 changes: 5 additions & 5 deletions src/Light.TemporaryStreams.Core/Hashing/HashingPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,18 @@ public async ValueTask DisposeAsync()
/// <param name="innerStream">The inner stream to be wrapped by the hash calculators.</param>
/// <param name="cancellationToken">The optional token to cancel the asynchronous operation.</param>
/// <returns>The outermost CryptoStream.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="innerStream" /> is null.</exception>
public ValueTask<Stream> SetUpAsync(Stream innerStream, CancellationToken cancellationToken = default)
{
innerStream.MustNotBeNull();
CryptoStream outermostStream = null!; // In the constructor, we ensure that the immutable array is not empty
Stream currentStream = innerStream;
for (var i = 0; i < HashCalculators.Length; i++)
{
outermostStream =
HashCalculators[i].CreateWrappingCryptoStream(innerStream, leaveWrappedStreamOpen: true);
currentStream = HashCalculators[i].CreateWrappingCryptoStream(currentStream, leaveWrappedStreamOpen: true);
}

_outermostCryptoStream = outermostStream;
return new ValueTask<Stream>(outermostStream);
_outermostCryptoStream = (CryptoStream)currentStream;
return new ValueTask<Stream>(currentStream);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static class TemporaryStreamServiceExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="temporaryStreamService" /> or <paramref name="source" /> are null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="copyBufferSize" /> has a value that is less than 0.</exception>
public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
this TemporaryStreamService temporaryStreamService,
this ITemporaryStreamService temporaryStreamService,
Stream source,
string? filePath = null,
TemporaryStreamServiceOptions? options = null,
Expand Down Expand Up @@ -100,7 +100,7 @@ await source
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="copyBufferSize" /> has a value that is less than 0.</exception>
/// <exception cref="EmptyCollectionException">Thrown when <paramref name="plugins" /> is empty or the default instance.</exception>
public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
this TemporaryStreamService temporaryStreamService,
this ITemporaryStreamService temporaryStreamService,
Stream source,
ImmutableArray<ICopyToTemporaryStreamPlugin> plugins,
string? filePath = null,
Expand All @@ -123,7 +123,7 @@ public static async Task<TemporaryStream> CopyToTemporaryStreamAsync(
{
for (var i = 0; i < plugins.Length; i++)
{
outermostStream = await plugins[i].SetUpAsync(source, cancellationToken).ConfigureAwait(false);
outermostStream = await plugins[i].SetUpAsync(outermostStream, cancellationToken).ConfigureAwait(false);
}

if (copyBufferSize.HasValue)
Expand Down
Loading