Add FileInfo.GetHash<T>

Wraps Stream.GetHash<T>, but saves the caller having to OpenRead themselves
This commit is contained in:
Oliver Booth 2022-04-20 14:29:45 +01:00
parent 5168948a1d
commit 374933e45e
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
3 changed files with 99 additions and 1 deletions

View File

@ -0,0 +1,48 @@
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,43 @@
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 file pointed at by <paramref name="value" /> does not exist.</exception>
/// <exception cref="IOException">The opened file stream cannot be read.</exception>
/// <exception cref="ArgumentException">
/// <para><typeparamref name="T" /> does not offer a static <c>Create</c> method.</para>
/// -or-
/// <para>
/// An invocation to the static <c>Create</c> method defined in <typeparamref name="T" /> returned
/// <see langword="null" />.
/// </para>
/// </exception>
public static byte[] GetHash<T>(this FileInfo value)
where T : HashAlgorithm
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (!value.Exists)
{
throw new FileNotFoundException("Cannot get hash of non-existent file.", value.FullName);
}
using FileStream stream = value.OpenRead();
return stream.GetHash<T>();
}
}

View File

@ -19,6 +19,7 @@ public static partial class StreamExtensions
/// <param name="stream">The stream whose hash is to be computed.</param> /// <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> /// <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="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>
/// <exception cref="IOException">The stream does not support reading.</exception>
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
/// <typeparamref name="T" /> does not offer a static <c>Create</c> method. /// <typeparamref name="T" /> does not offer a static <c>Create</c> method.
/// -or- /// -or-
@ -26,6 +27,10 @@ public static partial class StreamExtensions
/// <see langword="null" />. /// <see langword="null" />.
/// </exception> /// </exception>
/// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception> /// <exception cref="ObjectDisposedException">The stream has already been disposed.</exception>
/// <exception cref="TypeInitializationException">
/// The specified <see cref="HashAlgorithm" /> does not offer a parameterless <c>Create</c> method, or its <c>Create</c>
/// method returns a type that is not assignable to <typeparamref name="T" />.
/// </exception>
/// <remarks>This method consumes the stream from its current position!.</remarks> /// <remarks>This method consumes the stream from its current position!.</remarks>
public static byte[] GetHash<T>(this Stream stream) public static byte[] GetHash<T>(this Stream stream)
where T : HashAlgorithm where T : HashAlgorithm
@ -41,7 +46,9 @@ public static partial class StreamExtensions
} }
Type type = typeof(T); Type type = typeof(T);
MethodInfo? createMethod = type.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
MethodInfo? createMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(c => c.Name == "Create" && c.GetParameters().Length == 0);
if (createMethod is null) if (createMethod is null)
{ {
throw new TypeInitializationException(type.FullName, throw new TypeInitializationException(type.FullName,