From cfa27e0b4fb181ba948a9e6ce79e3774a80a55f9 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 22 Apr 2022 12:16:34 +0100 Subject: [PATCH] Move GetHash to child namespace (#7) Also introduces TryWriteHash --- CHANGELOG.md | 3 + X10D.Tests/src/Core/FileInfoTests.cs | 48 ------- X10D.Tests/src/IO/FileInfoTests.cs | 93 +++++++++++++ X10D.Tests/src/IO/StreamTests.cs | 59 +++++++++ .../FileInfoExtensions/FileInfoExtensions.cs | 35 ----- X10D/src/IO/FileInfoExtensions.cs | 74 +++++++++++ X10D/src/IO/StreamExtensions.cs | 122 ++++++++++++++++++ X10D/src/StreamExtensions/StreamExtensions.cs | 51 +------- 8 files changed, 352 insertions(+), 133 deletions(-) delete mode 100644 X10D.Tests/src/Core/FileInfoTests.cs create mode 100644 X10D.Tests/src/IO/FileInfoTests.cs create mode 100644 X10D.Tests/src/IO/StreamTests.cs delete mode 100644 X10D/src/FileInfoExtensions/FileInfoExtensions.cs create mode 100644 X10D/src/IO/FileInfoExtensions.cs create mode 100644 X10D/src/IO/StreamExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fa4bb2c..1aa2480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - 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 `FileInfo.GetHash()` +- Added `FileInfo.TryWriteHash(Span, out int)` - Added `IEnumerable.Product()` and `IEnumerable.Product(Func, TResult)` for all built-in numeric types, computing the product of all (transformed) elements - Added `IEnumerable.Shuffled([Random])`, which wraps `IList.Shuffle([Random])` and returns the result - Added `T.AsArray()` @@ -126,6 +127,8 @@ - Added `Type.SelectFromCustomAttribute()` - Added `DateTimeOffset` extensions which supersede `DateTime` extensions - Added `Endianness` enum +- Added `Stream.GetHash()` +- Added `Stream.TryWriteHash(Span, out int)` - Added `Stream.ReadInt16([Endian])` - Added `Stream.ReadInt32([Endian])` - Added `Stream.ReadInt64([Endian])` diff --git a/X10D.Tests/src/Core/FileInfoTests.cs b/X10D.Tests/src/Core/FileInfoTests.cs deleted file mode 100644 index 34362cb..0000000 --- a/X10D.Tests/src/Core/FileInfoTests.cs +++ /dev/null @@ -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(); - 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(() => ((FileInfo?)null)!.GetHash()); - } - - [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(() => new FileInfo(fileName).GetHash()); - } -} diff --git a/X10D.Tests/src/IO/FileInfoTests.cs b/X10D.Tests/src/IO/FileInfoTests.cs new file mode 100644 index 0000000..a1047ba --- /dev/null +++ b/X10D.Tests/src/IO/FileInfoTests.cs @@ -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(); + 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 hash = stackalloc byte[20]; + new FileInfo(fileName).TryWriteHash(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(() => ((FileInfo?)null)!.GetHash()); + Assert.ThrowsException(() => ((FileInfo?)null)!.TryWriteHash(Span.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(() => new FileInfo(fileName).GetHash()); + Assert.ThrowsException(() => new FileInfo(fileName).TryWriteHash(Span.Empty, out _)); + } +} diff --git a/X10D.Tests/src/IO/StreamTests.cs b/X10D.Tests/src/IO/StreamTests.cs new file mode 100644 index 0000000..402a854 --- /dev/null +++ b/X10D.Tests/src/IO/StreamTests.cs @@ -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(); + 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(() => ((Stream?)null)!.GetHash()); + Assert.ThrowsException(() => ((Stream?)null)!.TryWriteHash(Span.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 hash = stackalloc byte[20]; + stream.TryWriteHash(hash, out int bytesWritten); + Assert.AreEqual(expectedHash.Length, bytesWritten); + CollectionAssert.AreEqual(expectedHash, hash.ToArray()); + } +} diff --git a/X10D/src/FileInfoExtensions/FileInfoExtensions.cs b/X10D/src/FileInfoExtensions/FileInfoExtensions.cs deleted file mode 100644 index d79b498..0000000 --- a/X10D/src/FileInfoExtensions/FileInfoExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Security.Cryptography; - -namespace X10D; - -/// -/// Extension methods for . -/// -public static class FileInfoExtensions -{ - /// - /// Computes the hash of a file using the specified hash algorithm. - /// - /// The file whose hash to compute. - /// A derived type. - /// A array representing the hash of the file. - /// is . - /// The specified file was not found. - /// The opened file stream cannot be read. - /// - /// The specified does not offer a public, static. parameterless Create method, or its - /// Create method returns a type that is not assignable to . - /// - /// The stream has already been disposed. - public static byte[] GetHash(this FileInfo value) - where T : HashAlgorithm - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - using FileStream stream = value.OpenRead(); - return stream.GetHash(); - } -} diff --git a/X10D/src/IO/FileInfoExtensions.cs b/X10D/src/IO/FileInfoExtensions.cs new file mode 100644 index 0000000..9ecc323 --- /dev/null +++ b/X10D/src/IO/FileInfoExtensions.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; + +namespace X10D.IO; + +/// +/// IO-related extension methods for . +/// +public static class FileInfoExtensions +{ + /// + /// Computes the hash of a file using the specified hash algorithm. + /// + /// The file whose hash to compute. + /// + /// The type of the whose is to be used for + /// computing the hash. + /// + /// The hash of represented as an array of bytes. + /// is . + /// The specified file was not found. + /// The opened file stream cannot be read. + /// + /// The specified does not offer a public, static. parameterless Create method, or its + /// Create method returns a type that is not assignable to . + /// + /// The stream has already been disposed. + public static byte[] GetHash(this FileInfo value) + where T : HashAlgorithm + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + using FileStream stream = value.OpenRead(); + return stream.GetHash(); + } + + /// + /// Computes the hash of a file using the specified hash algorithm. + /// + /// The file whose hash to compute. + /// When this method returns, contains the computed hash of . + /// + /// When this method returns, the total number of bytes written into destination. This parameter is treated as + /// uninitialized. + /// + /// + /// The type of the whose is to be used for + /// computing the hash. + /// + /// + /// if the destination is long enough to receive the hash; otherwise, . + /// + /// is . + /// The specified file was not found. + /// The opened file stream cannot be read. + /// + /// The specified does not offer a public, static. parameterless Create method, or its + /// Create method returns a type that is not assignable to . + /// + /// The stream has already been disposed. + public static bool TryWriteHash(this FileInfo value, Span destination, out int bytesWritten) + where T : HashAlgorithm + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + using FileStream stream = value.OpenRead(); + return stream.TryWriteHash(destination, out bytesWritten); + } +} diff --git a/X10D/src/IO/StreamExtensions.cs b/X10D/src/IO/StreamExtensions.cs new file mode 100644 index 0000000..ce8a054 --- /dev/null +++ b/X10D/src/IO/StreamExtensions.cs @@ -0,0 +1,122 @@ +using System.Reflection; +using System.Security.Cryptography; + +namespace X10D.IO; + +/// +/// IO-related extension methods for . +/// +public static class StreamExtensions +{ + /// + /// Returns the hash of the current stream as an array of bytes using the specified hash algorithm. + /// + /// The stream whose hash is to be computed. + /// + /// The type of the whose is to be used for + /// computing the hash. + /// + /// The hash of represented as an array of bytes. + /// is + /// does not support reading. + /// + /// The specified does not offer a public, static. parameterless Create method, or its + /// Create method returns a type that is not assignable to . + /// + /// The stream has already been disposed. + public static byte[] GetHash(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); + } + + /// + /// Returns the hash of the current stream as an array of bytes using the specified hash algorithm. + /// + /// The stream whose hash is to be computed. + /// When this method returns, contains the computed hash of . + /// + /// When this method returns, the total number of bytes written into destination. This parameter is treated as + /// uninitialized. + /// + /// + /// The type of the whose is to be used for + /// computing the hash. + /// + /// + /// if the destination is long enough to receive the hash; otherwise, . + /// + /// is + /// does not support reading. + /// + /// The specified does not offer a public, static. parameterless Create method, or its + /// Create method returns a type that is not assignable to . + /// + /// The stream has already been disposed. + public static bool TryWriteHash(this Stream stream, Span 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 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); + } +} diff --git a/X10D/src/StreamExtensions/StreamExtensions.cs b/X10D/src/StreamExtensions/StreamExtensions.cs index 2b28af8..620dc32 100644 --- a/X10D/src/StreamExtensions/StreamExtensions.cs +++ b/X10D/src/StreamExtensions/StreamExtensions.cs @@ -1,6 +1,4 @@ -using System.Reflection; -using System.Security.Cryptography; -using System.Buffers.Binary; +using System.Buffers.Binary; namespace X10D; @@ -12,53 +10,6 @@ public static partial class StreamExtensions private static readonly Endianness DefaultEndianness = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; - /// - /// Returns the hash of a stream using the specified hash algorithm. - /// - /// A derived type. - /// The stream whose hash is to be computed. - /// A array representing the hash of the stream. - /// is . - /// The stream does not support reading. - /// - /// The specified does not offer a public, static. parameterless Create method, or its - /// Create method returns a type that is not assignable to . - /// - /// The stream has already been disposed. - /// This method consumes the stream from its current position!. - public static byte[] GetHash(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); - } - /// /// Reads a decimal value from the current stream using the system's default endian encoding, and advances the stream /// position by sixteen bytes.