Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Hashes out of Movie Files #2031

Merged
merged 7 commits into from
Nov 11, 2024
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
21 changes: 20 additions & 1 deletion TASVideos.Parsers/Parsers/Bk2.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace TASVideos.MovieParsers.Parsers;
using System.Security.Cryptography;

namespace TASVideos.MovieParsers.Parsers;

[FileExtension("bk2")]
internal class Bk2 : Parser, IParser
Expand Down Expand Up @@ -69,6 +71,23 @@ public async Task<IParseResult> Parse(Stream file, long length)
return Error("Could not determine the System Code");
}

string romHash = header.GetValueFor("SHA1");
if (string.IsNullOrEmpty(romHash))
{
romHash = header.GetValueFor("MD5");
}

HashType? hashType = romHash.Length switch {
2 * SHA1.HashSizeInBytes => HashType.Sha1,
2 * MD5.HashSizeInBytes => HashType.Md5,
8/* 2 * Crc32.HashLengthInBytes w/ System.IO.Hashing */ => HashType.Crc32,
_ => null
};
if (hashType is not null)
{
result.Hashes[hashType.Value] = romHash.ToLower();
}

int? rerecordVal = header.GetPositiveIntFor(Keys.RerecordCount);
if (rerecordVal.HasValue)
{
Expand Down
39 changes: 38 additions & 1 deletion TASVideos.Parsers/Parsers/Fm2.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace TASVideos.MovieParsers.Parsers;
using System.Text;

namespace TASVideos.MovieParsers.Parsers;

[FileExtension("fm2")]
internal class Fm2 : Parser, IParser
Expand Down Expand Up @@ -55,9 +57,43 @@ public async Task<IParseResult> Parse(Stream file, long length)
result.StartType = MovieStartType.Savestate;
}

var hashLine = header.GetValueFor(Keys.RomChecksum);
if (!string.IsNullOrWhiteSpace(hashLine))
{
var hashSplit = hashLine.Split(':');
var base64Line = hashSplit.Length == 2 ? hashSplit[1] : "";
if (!string.IsNullOrWhiteSpace(base64Line))
{
try
{
byte[] data = Convert.FromBase64String(base64Line);
string hash = BytesToHexString(data.AsSpan());
if (hash.Length == 32)
{
result.Hashes.Add(HashType.Md5, hash.ToLower());
}
}
catch
{
// Treat an invalid base64 hash as a missing hash
}
}
}

return result;
}

private static string BytesToHexString(ReadOnlySpan<byte> bytes)
{
StringBuilder sb = new(capacity: 2 * bytes.Length, maxCapacity: 2 * bytes.Length);
foreach (var b in bytes)
{
sb.Append($"{b:X2}");
}

return sb.ToString();
}

private static class Keys
{
public const string RerecordCount = "rerecordcount";
Expand All @@ -66,5 +102,6 @@ private static class Keys
public const string Length = "length";
public const string Fds = "fds";
public const string StartsFromSavestate = "savestate";
public const string RomChecksum = "romChecksum";
}
}
10 changes: 9 additions & 1 deletion TASVideos.Parsers/Parsers/Ltm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class Ltm : Parser, IParser
private const string VariableFramerateHeader = "variable_framerate=";
private const string LengthSecondsHeader = "length_sec=";
private const string LengthNanosecondsHeader = "length_nsec=";

private const string Md5 = "md5=";
public async Task<IParseResult> Parse(Stream file, long length)
{
var result = new SuccessResult(FileExtension)
Expand Down Expand Up @@ -94,6 +94,14 @@ public async Task<IParseResult> Parse(Stream file, long length)
{
lengthNanoseconds = ParseDoubleFromConfig(s);
}
else if (s.StartsWith(Md5))
{
var md5 = ParseStringFromConfig(s);
if (md5.Length == 32)
{
result.Hashes.Add(HashType.Md5, md5.ToLower());
}
}
}

break;
Expand Down
1 change: 1 addition & 0 deletions TASVideos.Parsers/Result/ErrorResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ internal class ErrorResult(string errorMsg) : IParseResult
public double? FrameRateOverride => null;
public long? CycleCount => null;
public string? Annotations => null;
public Dictionary<HashType, string> Hashes => [];
}
7 changes: 7 additions & 0 deletions TASVideos.Parsers/Result/IParseResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,11 @@ public interface IParseResult
/// Gets the annotations. These can be general comments, or other user entered descriptions supported by the file format.
/// </summary>
string? Annotations { get; }

Dictionary<HashType, string> Hashes { get; }
}

public enum HashType
{
Md5, Sha1, Sha256, Crc32
}
2 changes: 2 additions & 0 deletions TASVideos.Parsers/Result/SuccessResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal class SuccessResult(string fileExtension) : IParseResult
public string? Annotations { get; internal set; }

internal List<ParseWarnings> WarningList { get; } = [];

public Dictionary<HashType, string> Hashes { get; } = [];
}

internal static class ParseResultExtensions
Expand Down
1 change: 1 addition & 0 deletions tests/TASVideos.Core.Tests/Services/TestParseResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ internal class TestParseResult : IParseResult
public double? FrameRateOverride { get; init; }
public long? CycleCount { get; init; }
public string? Annotations { get; init; }
public Dictionary<HashType, string> Hashes { get; init; } = new Dictionary<HashType, string>();
}
1 change: 1 addition & 0 deletions tests/TASVideos.Core.Tests/Services/UserFilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,5 +256,6 @@ private class TestParseResult : IParseResult
public double? FrameRateOverride => null;
public long? CycleCount => null;
public string? Annotations => null;
public Dictionary<HashType, string> Hashes { get; init; } = new Dictionary<HashType, string>();
}
}
24 changes: 24 additions & 0 deletions tests/TASVideos.MovieParsers.Tests/Bk2ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,28 @@ public async Task Comments_ParseAsAnnotations()
var lines = result.Annotations.SplitWithEmpty("\n");
Assert.AreEqual(2, lines.Length);
}

[TestMethod]
[DataRow("hash-crc32-as-sha1", HashType.Crc32, "26b9ba0c")]
[DataRow("hash-crc32-as-md5", HashType.Crc32, "26b9ba0c")]
[DataRow("hash-md5-as-sha1", HashType.Md5, "811b027eaf99c2def7b933c5208636de")]
[DataRow("hash-md5", HashType.Md5, "811b027eaf99c2def7b933c5208636de")]
[DataRow("hash-sha1", HashType.Sha1, "ea343f4e445a9050d4b4fbac2c77d0693b1d0922")]
[DataRow("hash-sha1-as-md5", HashType.Sha1, "ea343f4e445a9050d4b4fbac2c77d0693b1d0922")]
public async Task Hashes(string filename, HashType hashType, string hash)
{
var result = await _bk2Parser.Parse(Embedded(filename + ".bk2"), EmbeddedLength(filename + ".bk2"));
Assert.AreEqual(1, result.Hashes.Count);
Assert.AreEqual(hashType, result.Hashes.First().Key);
Assert.AreEqual(hash, result.Hashes.First().Value);
}

[TestMethod]
[DataRow("hash-missing")]
[DataRow("hash-na")]
public async Task HashesMissing(string filename)
{
var result = await _bk2Parser.Parse(Embedded(filename + ".bk2"), EmbeddedLength(filename + ".bk2"));
Assert.AreEqual(0, result.Hashes.Count);
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/TASVideos.MovieParsers.Tests/Fm2ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,27 @@ public async Task BinaryWithoutFrameCount()
AssertNoWarnings(result);
Assert.AreEqual(1, result.Errors.Count());
}

[TestMethod]
public async Task Hash()
{
var result = await _fm2Parser.Parse(Embedded("hash.fm2"), EmbeddedLength("hash.fm2"));
Assert.AreEqual(1, result.Hashes.Count);
Assert.AreEqual(HashType.Md5, result.Hashes.First().Key);
Assert.AreEqual("e9d82f825725c616b0be66ac85dc1b7a", result.Hashes.First().Value);
}

[TestMethod]
public async Task InvalidHash()
{
var result = await _fm2Parser.Parse(Embedded("hash-invalid.fm2"), EmbeddedLength("hash-invalid.fm2"));
Assert.AreEqual(0, result.Hashes.Count);
}

[TestMethod]
public async Task MissingHash()
{
var result = await _fm2Parser.Parse(Embedded("hash-missing.fm2"), EmbeddedLength("hash-missing.fm2"));
Assert.AreEqual(0, result.Hashes.Count);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
romChecksum base64:ThisIsNotBase64

Empty file.
1 change: 1 addition & 0 deletions tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash.fm2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
romChecksum base64:6DgvglcLxhawvMAshDwbeQ==
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
30 changes: 30 additions & 0 deletions tests/TASVideos.MovieParsers.Tests/LtmTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,34 @@ public async Task VariableFramerate()
Assert.AreEqual(30.002721239119342, result.FrameRateOverride);
AssertNoWarningsOrErrors(result);
}

[TestMethod]
public async Task Hash()
{
var result = await _ltmParser.Parse(Embedded("hash.ltm"), EmbeddedLength("hash.ltm"));
Assert.AreEqual(1, result.Hashes.Count);
Assert.AreEqual(HashType.Md5, result.Hashes.First().Key);
Assert.AreEqual("7d66e47fdc0807927c40ce1491c68ad3", result.Hashes.First().Value);
}

[TestMethod]
public async Task NoHash()
{
var result = await _ltmParser.Parse(Embedded("no-hash.ltm"), EmbeddedLength("no-hash.ltm"));
Assert.AreEqual(0, result.Hashes.Count);
}

[TestMethod]
public async Task MissingHash()
{
var result = await _ltmParser.Parse(Embedded("missing-hash.ltm"), EmbeddedLength("missing-hash.ltm"));
Assert.AreEqual(0, result.Hashes.Count);
}

[TestMethod]
public async Task InvalidHash()
{
var result = await _ltmParser.Parse(Embedded("invalid-hash.ltm"), EmbeddedLength("invalid-hash.ltm"));
Assert.AreEqual(0, result.Hashes.Count);
}
}
Loading