Skip to content

Commit

Permalink
Hasher tests & hasher method renaming
Browse files Browse the repository at this point in the history
Also changed SHA256 Hash creation, it now takes 92% less memory!
Added IDisposable to TuupUpdatePackage
Added "External Test", making it so it's test source can be found from the class that is actually holding the tests and not the abstracted classes
  • Loading branch information
Azyyyyyy committed Jan 10, 2024
1 parent eeb9e12 commit 50645a1
Show file tree
Hide file tree
Showing 24 changed files with 412 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/Deltas/TinyUpdate.Delta.MSDelta/Struct/DeltaHash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public string GetHash()
fixed (byte* hashPtr = HashValue)
{
using var unmanagedStream = new UnmanagedMemoryStream(hashPtr, HashSize);
return SHA256.Instance.CreateHash(unmanagedStream);
return SHA256.Instance.HashData(unmanagedStream);
}
}
}
34 changes: 25 additions & 9 deletions src/Packages/TinyUpdate.TUUP/TuupUpdatePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ namespace TinyUpdate.TUUP;
/// <summary>
/// Update Package based on TinyUpdate V1, Makes use of zip format
/// </summary>
public class TuupUpdatePackage(IDeltaManager deltaManager, IHasher hasher) : IUpdatePackage
public class TuupUpdatePackage(IDeltaManager deltaManager, IHasher hasher) : IUpdatePackage, IDisposable
{
private static readonly string[] ExpectedData = ["Filename", "Path", "Hash", "Filesize", "Extension"];

private bool _loaded;
private bool _disposed;
private ZipArchive? _zipArchive;
private static readonly string[] ExpectedData = ["Filename", "Path", "Hash", "Filesize", "Extension"];

public string Extension => Consts.TuupExtension;
public ICollection<FileEntry> DeltaFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> UnchangedFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> NewFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> MovedFiles { get; } = new List<FileEntry>();

public async Task Load(Stream updatePackageStream)
{
Expand Down Expand Up @@ -52,12 +58,7 @@ public async Task Load(Stream updatePackageStream)

_loaded = true;
}

public ICollection<FileEntry> DeltaFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> UnchangedFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> NewFiles { get; } = new List<FileEntry>();
public ICollection<FileEntry> MovedFiles { get; } = new List<FileEntry>();


/// <summary>
/// Gets all the files that this update will have and any information needed to correctly apply the update
/// </summary>
Expand Down Expand Up @@ -139,7 +140,7 @@ private async IAsyncEnumerable<FileEntry> GetFilesFromPackage(ZipArchive zip)
}
}

private bool HasAllFileEntryData(Dictionary<string, object?> fileEntryData)
private static bool HasAllFileEntryData(Dictionary<string, object?> fileEntryData)
{
var missingCore = ExpectedData.Except(fileEntryData.Keys).Any();
if (missingCore)
Expand All @@ -166,4 +167,19 @@ private bool HasAllFileEntryData(Dictionary<string, object?> fileEntryData)
fileEntryData.TryAdd("PreviousLocation", null);
return true;
}

public void Dispose()
{
if (!_disposed)
{
if (_loaded)
{
_zipArchive?.Dispose();
_loaded = false;
}

GC.SuppressFinalize(this);
_disposed = true;
}
}
}
4 changes: 2 additions & 2 deletions src/Packages/TinyUpdate.TUUP/TuupUpdatePackageCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ async Task GetHashes(IDictionary<string, List<string>> hashes, IEnumerable<strin
foreach (var oldFile in files)
{
await using var oldFileContentStream = File.OpenRead(oldFile);
var hash = _hasher.CreateHash(oldFileContentStream);
var hash = _hasher.HashData(oldFileContentStream);
if (!hashes.TryGetValue(hash, out var filesList))
{
filesList = new List<string>();
Expand Down Expand Up @@ -185,7 +185,7 @@ private async Task<bool> AddFile(ZipArchive zipArchive, Stream fileContentStream
await zipFileEntryStream.DisposeAsync();
}

hash ??= _hasher.CreateHash(fileContentStream);
hash ??= _hasher.HashData(fileContentStream);
await AddHashAndSizeData(zipArchive, filepath, hash, fileContentStream.Length);

return true;
Expand Down
8 changes: 4 additions & 4 deletions src/TinyUpdate.Core/Abstract/IHasher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ public interface IHasher
/// <param name="stream"><see cref="Stream"/> to check against</param>
/// <param name="expectedHash">Hash that we are expecting</param>
/// <returns>If the <see cref="Stream"/> outputs the same hash as we are expecting</returns>
bool CheckHash(Stream stream, string expectedHash);
bool CompareHash(Stream stream, string expectedHash);

/// <summary>
/// Checks a <see cref="byte"/>[] to a hash that is expected
/// </summary>
/// <param name="byteArray"><see cref="byte"/>[] to check against</param>
/// <param name="expectedHash">Hash that we are expecting</param>
/// <returns>If the <see cref="byte"/>[] outputs the same hash as we are expecting</returns>
bool CheckHash(byte[] byteArray, string expectedHash);
bool CompareHash(byte[] byteArray, string expectedHash);

/// <summary>
/// Creates a hash from a <see cref="Stream"/>
/// </summary>
/// <param name="stream"><see cref="Stream"/> to create hash for</param>
string CreateHash(Stream stream);
string HashData(Stream stream);

/// <summary>
/// Creates a hash from a <see cref="byte"/>[]
/// </summary>
/// <param name="bytes"><see cref="byte"/>[] to use for creating hash</param>
string CreateHash(byte[] bytes);
string HashData(byte[] bytes);

/// <summary>
/// Gets if this string is a valid hash
Expand Down
73 changes: 48 additions & 25 deletions src/TinyUpdate.Core/SHA256.cs
Original file line number Diff line number Diff line change
@@ -1,58 +1,81 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using TinyUpdate.Core.Abstract;

namespace TinyUpdate.Core;

/// <summary>
/// Easy access to processing Streams into a SHA256 hash and comparing SHA256 hashes
/// </summary>
public partial class SHA256(ILogger logger) : IHasher
public partial class SHA256 : IHasher
{
public static readonly SHA256 Instance = new SHA256(NullLogger.Instance);

private const string EmptyHash = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";
private static readonly Regex Sha256Regex = MyRegex();

public bool CheckHash(Stream stream, string expectedHash)
{
stream.Seek(0, SeekOrigin.Begin);
var hash = CreateHash(stream);
return hash == expectedHash;
}
public static readonly SHA256 Instance = new SHA256();

public bool CheckHash(byte[] byteArray, string expectedHash)
public bool CompareHash(Stream stream, string expectedHash)
{
if (IsValidHash(expectedHash))
{
var sameHash = CreateHash(byteArray) == expectedHash;
logger.LogInformation("Do we have the expected SHA256 hash?: {SameHash}", sameHash);
return sameHash;
var hash = HashData(stream);
return hash == expectedHash;
}

logger.LogWarning("We been given an invalid hash, can't check");
return false;
}

public string CreateHash(Stream stream)
public bool CompareHash(byte[] byteArray, string expectedHash)
{
stream.Seek(0, SeekOrigin.Begin);
using var sha256 = System.Security.Cryptography.SHA256.Create();
return CreateHash(sha256.ComputeHash(stream));
if (IsValidHash(expectedHash))
{
var hash = HashData(byteArray, true);
return hash == expectedHash;
}

return false;
}

public string CreateHash(byte[] bytes)
public string HashData(Stream stream)
{
string result = string.Empty;
foreach (var b in bytes)
//If we got nothing then return this, this will always be calculated by below
if (stream is { CanSeek: true, Length: 0 })
{
result += b.ToString("X2");
return EmptyHash;
}
return result;

var dataHashed = System.Security.Cryptography.SHA256.HashData(stream);
return HashData(dataHashed, false);

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference return.

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference return.

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference return.

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference return.

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference return.

Check warning on line 47 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference return.
}

public string HashData(byte[] bytes) => HashData(bytes, true);

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference return.

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference return.

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference return.

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference return.

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference return.

Check warning on line 50 in src/TinyUpdate.Core/SHA256.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference return.

public bool IsValidHash(string hash) => !string.IsNullOrWhiteSpace(hash) && Sha256Regex.IsMatch(hash);

[GeneratedRegex("^[a-fA-F0-9]{64}$", RegexOptions.Compiled)]
private static partial Regex MyRegex();

private static string? HashData(byte[] bytes, bool processBytes)
{
if (processBytes)
{
//If we got nothing then return this, this will always be calculated by below
if (bytes.Length == 0)
{
return EmptyHash;
}

using var memStream = new MemoryStream(bytes);
bytes = System.Security.Cryptography.SHA256.HashData(memStream);
}

var resultArray = new Span<char>(new char[64]);
var charsWritten = 0;

foreach (var @byte in bytes)
{
@byte.TryFormat(resultArray[charsWritten..], out var written, "X2");
charsWritten += written;
}
return resultArray.ToString();
}
}
44 changes: 44 additions & 0 deletions tests/TinyUpdate.Core.Tests/Abstract/HasherCan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

using TinyUpdate.Core.Abstract;
using TinyUpdate.Core.Tests.Attributes;

namespace TinyUpdate.Core.Tests.Abstract;

public abstract class HasherCan(IHasher hasher)
{
private IHasher Hasher { get; } = hasher;

[ExternalTest]
public void CompareCorrectly_Stream(bool expectedStatus, string expectedHash, Stream streamToHash)
{
var hash = Hasher.CompareHash(streamToHash, expectedHash);
Assert.That(hash, Is.EqualTo(expectedStatus));
}

[ExternalTest]
public void CompareCorrectly_Array(bool expectedStatus, string expectedHash, byte[] arrayToHash)
{
var hash = Hasher.CompareHash(arrayToHash, expectedHash);
Assert.That(hash, Is.EqualTo(expectedStatus));
}

[ExternalTest]
public void ReturnCorrectHash_Stream(string expectedHash, Stream streamToHash)
{
var hash = Hasher.HashData(streamToHash);
Assert.That(hash, Is.EqualTo(expectedHash));
}

[ExternalTest]
public void ReturnCorrectHash_Array(string expectedHash, byte[] arrayToHash)
{
var hash = Hasher.HashData(arrayToHash);
Assert.That(hash, Is.EqualTo(expectedHash));
}

[ExternalTest]
public void ValidateHashCorrectly(string hash, bool expectedValidation)
{
Assert.That(Hasher.IsValidHash(hash), Is.EqualTo(expectedValidation));
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Collections;
using System.Globalization;
using System.Reflection;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using NUnit.Framework.Internal.Builders;

namespace TinyUpdate.Core.Tests.Attributes;

//Readopted TestCaseSourceAttribute to make this
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class DynamicTestCaseSourceAttribute : Attribute, ITestBuilder
{
private readonly NUnitTestCaseBuilder _builder = new();

public DynamicTestCaseSourceAttribute(string testName, Type sourceType, string methodName, object?[]? parameters = null)
{
TestName = testName;
SourceType = sourceType;
MethodName = methodName;
Parameters = parameters;
}

public string TestName { get; }

public Type SourceType { get; }

public string MethodName { get; }

public object?[]? Parameters { get; }

public IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test? suite)
{
if (method.Name != TestName) yield break;

var count = 0;
foreach (var parms in GetTestCasesFor(method))
{
count++;
yield return _builder.BuildTestMethod(method, suite, parms);
}

// If count > 0, error messages will be shown for each case
// but if it's 0, we need to add an extra "test" to show the message.
if (count == 0 && method.GetParameters().Length == 0)
{
var parms = new TestCaseParameters { RunState = RunState.NotRunnable };
parms.Properties.Set(PropertyNames.SkipReason, "DynamicTestCaseSource may not be used on a method without parameters");

yield return _builder.BuildTestMethod(method, suite, parms);
}
}

private IEnumerable<TestCaseParameters> GetTestCasesFor(IMethodInfo method)
{
var methodEnumerable = SourceType.GetMethod(MethodName)?.Invoke(null, BindingFlags.Public | BindingFlags.Static,
null, Parameters, CultureInfo.CurrentCulture) as IEnumerable;
if (methodEnumerable == null)
{
var parms = new TestCaseParameters { RunState = RunState.NotRunnable };
parms.Properties.Set(PropertyNames.SkipReason, "DynamicTestCaseSource can't find the method to invoke");

yield return parms;
yield break;
}

foreach (var item in methodEnumerable)
{
// First handle two easy cases:
// 1. Source is null. This is really an error but if we
// throw an exception we simply get an invalid fixture
// without good info as to what caused it. Passing a
// single null argument will cause an error to be
// reported at the test level, in most cases.
// 2. User provided an TestCaseParameters and we just use it.
var parms = item is null
? new TestCaseParameters(new object?[] { null })
: item as TestCaseParameters;

if (parms is not null)
{
yield return parms;
continue;
}

object?[]? args = null;

// 3. An array was passed, it may be an object[]
// or possibly some other kind of array, which
// TestCaseSource can accept.
if (item is Array array)
{
// If array has the same number of elements as parameters
// and it does not fit exactly into single existing parameter
// we believe that this array contains arguments, not is a bare
// argument itself.
var parameters = method.GetParameters();
var argsNeeded = parameters.Length;
if (argsNeeded > 0 && argsNeeded == array.Length && parameters[0].ParameterType != array.GetType())
{
args = new object?[array.Length];
for (var i = 0; i < array.Length; i++)
args[i] = array.GetValue(i);
}
}

args ??= new[] { item };
yield return new TestCaseParameters(args);
}
}
}
Loading

0 comments on commit 50645a1

Please sign in to comment.