Move GetHash to child namespace (#7)

Also introduces TryWriteHash
This commit is contained in:
Oliver Booth 2022-04-22 12:16:34 +01:00
parent ae24d94da2
commit cfa27e0b4f
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
8 changed files with 352 additions and 133 deletions

View File

@ -10,6 +10,7 @@
- Added time-related extension methods (`Ticks`, `Milliseconds`, `Seconds`, `Minutes`, `Hours`, `Days`, and `Weeks`) to all built-in numeric types - Added time-related extension methods (`Ticks`, `Milliseconds`, `Seconds`, `Minutes`, `Hours`, `Days`, and `Weeks`) to all built-in numeric types
- Added `TimeSpan.Ago()` and `TimeSpan.FromNow()` - Added `TimeSpan.Ago()` and `TimeSpan.FromNow()`
- Added `FileInfo.GetHash<T>()` - Added `FileInfo.GetHash<T>()`
- Added `FileInfo.TryWriteHash<T>(Span<byte>, out int)`
- Added `IEnumerable<TSource>.Product()` and `IEnumerable<TSource>.Product<TResult>(Func<TSource>, TResult)` for all built-in numeric types, computing the product of all (transformed) elements - Added `IEnumerable<TSource>.Product()` and `IEnumerable<TSource>.Product<TResult>(Func<TSource>, TResult)` for all built-in numeric types, computing the product of all (transformed) elements
- Added `IEnumerable<T>.Shuffled([Random])`, which wraps `IList<T>.Shuffle([Random])` and returns the result - Added `IEnumerable<T>.Shuffled([Random])`, which wraps `IList<T>.Shuffle([Random])` and returns the result
- Added `T.AsArray()` - Added `T.AsArray()`
@ -126,6 +127,8 @@
- Added `Type.SelectFromCustomAttribute<TAttribute, TReturn>()` - Added `Type.SelectFromCustomAttribute<TAttribute, TReturn>()`
- Added `DateTimeOffset` extensions which supersede `DateTime` extensions - Added `DateTimeOffset` extensions which supersede `DateTime` extensions
- Added `Endianness` enum - Added `Endianness` enum
- Added `Stream.GetHash<T>()`
- Added `Stream.TryWriteHash<T>(Span<byte>, out int)`
- Added `Stream.ReadInt16([Endian])` - Added `Stream.ReadInt16([Endian])`
- Added `Stream.ReadInt32([Endian])` - Added `Stream.ReadInt32([Endian])`
- Added `Stream.ReadInt64([Endian])` - Added `Stream.ReadInt64([Endian])`

View File

@ -1,48 +0,0 @@
using System.Security.Cryptography;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace X10D.Tests.Core;
[TestClass]
public class FileInfoTests
{
[TestMethod]
public void GetHash()
{
string fileName = $"temp.{DateTimeOffset.Now.ToUnixTimeSeconds()}.bin";
if (File.Exists(fileName))
{
Assert.Fail("Temporary file already exists");
}
const string expectedHash = "0A4D55A8D778E5022FAB701977C5D840BBC486D0"; // SHA-1
File.WriteAllText(fileName, "Hello World");
Assert.IsTrue(File.Exists(fileName), $"File.Exists(\"{fileName}\")");
byte[] hash = new FileInfo(fileName).GetHash<SHA1>();
string actualHash = BitConverter.ToString(hash).Replace("-", "").ToUpperInvariant();
Assert.AreEqual(expectedHash, actualHash);
File.Delete(fileName); // cleanup is important
}
[TestMethod]
public void GetHash_NullFileInfo()
{
// any HashAlgorithm will do, but SHA1 is used above. so to remain consistent, we use it here
Assert.ThrowsException<ArgumentNullException>(() => ((FileInfo?)null)!.GetHash<SHA1>());
}
[TestMethod]
public void GetHash_InvalidFile()
{
string fileName = $"temp.{DateTimeOffset.Now.ToUnixTimeSeconds()}.bin";
if (File.Exists(fileName))
{
Assert.Fail("Temporary file already exists");
}
// any HashAlgorithm will do, but SHA1 is used above. so to remain consistent, we use it here
Assert.ThrowsException<FileNotFoundException>(() => new FileInfo(fileName).GetHash<SHA1>());
}
}

View File

@ -0,0 +1,93 @@
using System.Security.Cryptography;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.IO;
namespace X10D.Tests.IO;
[TestClass]
public class FileInfoTests
{
[TestMethod]
public void GetHashSha1ShouldBeCorrect()
{
string fileName = $"temp.{DateTimeOffset.Now.ToUnixTimeSeconds()}.bin";
if (File.Exists(fileName))
{
Assert.Fail("Temporary file already exists");
}
File.WriteAllText(fileName, "Hello World");
Assert.IsTrue(File.Exists(fileName));
// SHA-1
byte[] expectedHash =
{
0x0A, 0x4D, 0x55, 0xA8, 0xD7, 0x78, 0xE5, 0x02, 0x2F, 0xAB, 0x70, 0x19, 0x77, 0xC5, 0xD8, 0x40, 0xBB, 0xC4, 0x86,
0xD0
};
try
{
byte[] hash = new FileInfo(fileName).GetHash<SHA1>();
CollectionAssert.AreEqual(expectedHash, hash);
}
finally
{
File.Delete(fileName); // cleanup is important
}
}
[TestMethod]
public void TryWriteHashSha1ShouldBeCorrect()
{
string fileName = $"temp.{DateTimeOffset.Now.ToUnixTimeSeconds()}.bin";
if (File.Exists(fileName))
{
Assert.Fail("Temporary file already exists");
}
File.WriteAllText(fileName, "Hello World");
Assert.IsTrue(File.Exists(fileName));
// SHA-1
byte[] expectedHash =
{
0x0A, 0x4D, 0x55, 0xA8, 0xD7, 0x78, 0xE5, 0x02, 0x2F, 0xAB, 0x70, 0x19, 0x77, 0xC5, 0xD8, 0x40, 0xBB, 0xC4, 0x86,
0xD0
};
try
{
Span<byte> hash = stackalloc byte[20];
new FileInfo(fileName).TryWriteHash<SHA1>(hash, out int bytesWritten);
Assert.AreEqual(expectedHash.Length, bytesWritten);
CollectionAssert.AreEqual(expectedHash, hash.ToArray());
}
finally
{
File.Delete(fileName); // cleanup is important
}
}
[TestMethod]
public void GetHashNullShouldThrow()
{
// any HashAlgorithm will do, but SHA1 is used above. so to remain consistent, we use it here
Assert.ThrowsException<ArgumentNullException>(() => ((FileInfo?)null)!.GetHash<SHA1>());
Assert.ThrowsException<ArgumentNullException>(() => ((FileInfo?)null)!.TryWriteHash<SHA1>(Span<byte>.Empty, out _));
}
[TestMethod]
public void GetHashInvalidFileShouldThrow()
{
string fileName = $"temp.{DateTimeOffset.Now.ToUnixTimeSeconds()}.bin";
if (File.Exists(fileName))
{
Assert.Fail("Temporary file already exists");
}
// any HashAlgorithm will do, but SHA1 is used above. so to remain consistent, we use it here
Assert.ThrowsException<FileNotFoundException>(() => new FileInfo(fileName).GetHash<SHA1>());
Assert.ThrowsException<FileNotFoundException>(() => new FileInfo(fileName).TryWriteHash<SHA1>(Span<byte>.Empty, out _));
}
}

View File

@ -0,0 +1,59 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.IO;
namespace X10D.Tests.IO;
[TestClass]
public class StreamTests
{
[TestMethod]
public void GetHashSha1ShouldBeCorrect()
{
// SHA-1
byte[] expectedHash =
{
0x0A, 0x4D, 0x55, 0xA8, 0xD7, 0x78, 0xE5, 0x02, 0x2F, 0xAB, 0x70, 0x19, 0x77, 0xC5, 0xD8, 0x40, 0xBB, 0xC4, 0x86,
0xD0
};
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes("Hello World"));
stream.Position = 0;
byte[] hash = stream.GetHash<SHA1>();
Trace.WriteLine($"Hash: {BitConverter.ToString(hash)}");
Trace.WriteLine($"Expected: {BitConverter.ToString(expectedHash)}");
CollectionAssert.AreEqual(expectedHash, hash);
}
[TestMethod]
public void GetHashNullShouldThrow()
{
// any HashAlgorithm will do, but SHA1 is used above. so to remain consistent, we use it here
Assert.ThrowsException<ArgumentNullException>(() => ((Stream?)null)!.GetHash<SHA1>());
Assert.ThrowsException<ArgumentNullException>(() => ((Stream?)null)!.TryWriteHash<SHA1>(Span<byte>.Empty, out _));
}
[TestMethod]
public void TryWriteHashSha1ShouldBeCorrect()
{
// SHA-1
byte[] expectedHash =
{
0x0A, 0x4D, 0x55, 0xA8, 0xD7, 0x78, 0xE5, 0x02, 0x2F, 0xAB, 0x70, 0x19, 0x77, 0xC5, 0xD8, 0x40, 0xBB, 0xC4, 0x86,
0xD0
};
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes("Hello World"));
stream.Position = 0;
Span<byte> hash = stackalloc byte[20];
stream.TryWriteHash<SHA1>(hash, out int bytesWritten);
Assert.AreEqual(expectedHash.Length, bytesWritten);
CollectionAssert.AreEqual(expectedHash, hash.ToArray());
}
}

View File

@ -1,35 +0,0 @@
using System.Security.Cryptography;
namespace X10D;
/// <summary>
/// Extension methods for <see cref="FileInfo" />.
/// </summary>
public static class FileInfoExtensions
{
/// <summary>
/// Computes the hash of a file using the specified hash algorithm.
/// </summary>
/// <param name="value">The file whose hash to compute.</param>
/// <typeparam name="T">A <see cref="HashAlgorithm" /> derived type.</typeparam>
/// <returns>A <see cref="byte" /> array representing the hash of the file.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
/// <exception cref="FileNotFoundException">The specified file was not found.</exception>
/// <exception cref="IOException">The opened file stream cannot be read.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
public static byte[] GetHash<T>(this FileInfo value)
where T : HashAlgorithm
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
using FileStream stream = value.OpenRead();
return stream.GetHash<T>();
}
}

View File

@ -0,0 +1,74 @@
using System.Security.Cryptography;
namespace X10D.IO;
/// <summary>
/// IO-related extension methods for <see cref="FileInfo" />.
/// </summary>
public static class FileInfoExtensions
{
/// <summary>
/// Computes the hash of a file using the specified hash algorithm.
/// </summary>
/// <param name="value">The file whose hash to compute.</param>
/// <typeparam name="T">
/// The type of the <see cref="HashAlgorithm" /> whose <see cref="HashAlgorithm.ComputeHash(Stream)" /> is to be used for
/// computing the hash.
/// </typeparam>
/// <returns>The hash of <paramref name="stream" /> represented as an array of bytes.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
/// <exception cref="FileNotFoundException">The specified file was not found.</exception>
/// <exception cref="IOException">The opened file stream cannot be read.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
public static byte[] GetHash<T>(this FileInfo value)
where T : HashAlgorithm
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
using FileStream stream = value.OpenRead();
return stream.GetHash<T>();
}
/// <summary>
/// Computes the hash of a file using the specified hash algorithm.
/// </summary>
/// <param name="value">The file whose hash to compute.</param>
/// <param name="destination">When this method returns, contains the computed hash of <paramref name="value" />.</param>
/// <param name="bytesWritten">
/// When this method returns, the total number of bytes written into destination. This parameter is treated as
/// uninitialized.
/// </param>
/// <typeparam name="T">
/// The type of the <see cref="HashAlgorithm" /> whose <see cref="HashAlgorithm.ComputeHash(Stream)" /> is to be used for
/// computing the hash.
/// </typeparam>
/// <returns>
/// <see langword="true" /> if the destination is long enough to receive the hash; otherwise, <see langword="false" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
/// <exception cref="FileNotFoundException">The specified file was not found.</exception>
/// <exception cref="IOException">The opened file stream cannot be read.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
public static bool TryWriteHash<T>(this FileInfo value, Span<byte> destination, out int bytesWritten)
where T : HashAlgorithm
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
using FileStream stream = value.OpenRead();
return stream.TryWriteHash<T>(destination, out bytesWritten);
}
}

View File

@ -0,0 +1,122 @@
using System.Reflection;
using System.Security.Cryptography;
namespace X10D.IO;
/// <summary>
/// IO-related extension methods for <see cref="Stream" />.
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Returns the hash of the current stream as an array of bytes using the specified hash algorithm.
/// </summary>
/// <param name="stream">The stream whose hash is to be computed.</param>
/// <typeparam name="T">
/// The type of the <see cref="HashAlgorithm" /> whose <see cref="HashAlgorithm.ComputeHash(Stream)" /> is to be used for
/// computing the hash.
/// </typeparam>
/// <returns>The hash of <paramref name="stream" /> represented as an array of bytes.</returns>
/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" /></exception>
/// <exception cref="IOException"><paramref name="stream" /> does not support reading.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
public static byte[] GetHash<T>(this Stream stream)
where T : HashAlgorithm
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new IOException(ExceptionMessages.StreamDoesNotSupportReading);
}
Type type = typeof(T);
MethodInfo? createMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(c => c.Name == "Create" && c.GetParameters().Length == 0);
if (createMethod is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmNoCreateMethod));
}
using var crypt = createMethod.Invoke(null, null) as T;
if (crypt is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmCreateReturnedNull));
}
return crypt.ComputeHash(stream);
}
/// <summary>
/// Returns the hash of the current stream as an array of bytes using the specified hash algorithm.
/// </summary>
/// <param name="stream">The stream whose hash is to be computed.</param>
/// <param name="destination">When this method returns, contains the computed hash of <paramref name="stream" />.</param>
/// <param name="bytesWritten">
/// When this method returns, the total number of bytes written into destination. This parameter is treated as
/// uninitialized.
/// </param>
/// <typeparam name="T">
/// The type of the <see cref="HashAlgorithm" /> whose <see cref="HashAlgorithm.ComputeHash(Stream)" /> is to be used for
/// computing the hash.
/// </typeparam>
/// <returns>
/// <see langword="true" /> if the destination is long enough to receive the hash; otherwise, <see langword="false" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" /></exception>
/// <exception cref="IOException"><paramref name="stream" /> does not support reading.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
public static bool TryWriteHash<T>(this Stream stream, Span<byte> destination, out int bytesWritten)
where T : HashAlgorithm
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new IOException(ExceptionMessages.StreamDoesNotSupportReading);
}
Type type = typeof(T);
MethodInfo? createMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(c => c.Name == "Create" && c.GetParameters().Length == 0);
if (createMethod is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmNoCreateMethod));
}
using var crypt = createMethod.Invoke(null, null) as T;
if (crypt is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmCreateReturnedNull));
}
if (stream.Length > int.MaxValue)
{
throw new ArgumentException(ExceptionMessages.StreamTooLarge);
}
Span<byte> buffer = stackalloc byte[(int)stream.Length];
_ = stream.Read(buffer); // we don't care about the number of bytes read. we can ignore MustUseReturnValue
return crypt.TryComputeHash(buffer, destination, out bytesWritten);
}
}

View File

@ -1,6 +1,4 @@
using System.Reflection; using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Buffers.Binary;
namespace X10D; namespace X10D;
@ -12,53 +10,6 @@ public static partial class StreamExtensions
private static readonly Endianness DefaultEndianness = private static readonly Endianness DefaultEndianness =
BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian;
/// <summary>
/// Returns the hash of a stream using the specified hash algorithm.
/// </summary>
/// <typeparam name="T">A <see cref="HashAlgorithm" /> derived type.</typeparam>
/// <param name="stream">The stream whose hash is to be computed.</param>
/// <returns>A <see cref="byte" /> array representing the hash of the stream.</returns>
/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>
/// <exception cref="IOException">The stream does not support reading.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a public, static. parameterless <c>Create</c> method, or its
/// <c>Create</c> method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
/// <remarks>This method consumes the stream from its current position!.</remarks>
public static byte[] GetHash<T>(this Stream stream)
where T : HashAlgorithm
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new IOException(ExceptionMessages.StreamDoesNotSupportReading);
}
Type type = typeof(T);
MethodInfo? createMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(c => c.Name == "Create" && c.GetParameters().Length == 0);
if (createMethod is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmNoCreateMethod));
}
using var crypt = createMethod.Invoke(null, null) as T;
if (crypt is null)
{
throw new TypeInitializationException(type.FullName,
new ArgumentException(ExceptionMessages.HashAlgorithmCreateReturnedNull));
}
return crypt.ComputeHash(stream);
}
/// <summary> /// <summary>
/// Reads a decimal value from the current stream using the system's default endian encoding, and advances the stream /// Reads a decimal value from the current stream using the system's default endian encoding, and advances the stream
/// position by sixteen bytes. /// position by sixteen bytes.