diff --git a/TASVideos.Parsers/Parsers/Bk2.cs b/TASVideos.Parsers/Parsers/Bk2.cs index bf166153e..237c82175 100644 --- a/TASVideos.Parsers/Parsers/Bk2.cs +++ b/TASVideos.Parsers/Parsers/Bk2.cs @@ -1,4 +1,6 @@ -namespace TASVideos.MovieParsers.Parsers; +using System.Security.Cryptography; + +namespace TASVideos.MovieParsers.Parsers; [FileExtension("bk2")] internal class Bk2 : Parser, IParser @@ -69,6 +71,23 @@ public async Task 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) { diff --git a/TASVideos.Parsers/Parsers/Fm2.cs b/TASVideos.Parsers/Parsers/Fm2.cs index 33fb91233..fbc516578 100644 --- a/TASVideos.Parsers/Parsers/Fm2.cs +++ b/TASVideos.Parsers/Parsers/Fm2.cs @@ -1,4 +1,6 @@ -namespace TASVideos.MovieParsers.Parsers; +using System.Text; + +namespace TASVideos.MovieParsers.Parsers; [FileExtension("fm2")] internal class Fm2 : Parser, IParser @@ -55,9 +57,43 @@ public async Task 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 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"; @@ -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"; } } diff --git a/TASVideos.Parsers/Parsers/Ltm.cs b/TASVideos.Parsers/Parsers/Ltm.cs index 52e26b6f7..30160d7ac 100644 --- a/TASVideos.Parsers/Parsers/Ltm.cs +++ b/TASVideos.Parsers/Parsers/Ltm.cs @@ -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 Parse(Stream file, long length) { var result = new SuccessResult(FileExtension) @@ -94,6 +94,14 @@ public async Task 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; diff --git a/TASVideos.Parsers/Result/ErrorResult.cs b/TASVideos.Parsers/Result/ErrorResult.cs index 7f708db12..098c8b0c1 100644 --- a/TASVideos.Parsers/Result/ErrorResult.cs +++ b/TASVideos.Parsers/Result/ErrorResult.cs @@ -18,4 +18,5 @@ internal class ErrorResult(string errorMsg) : IParseResult public double? FrameRateOverride => null; public long? CycleCount => null; public string? Annotations => null; + public Dictionary Hashes => []; } diff --git a/TASVideos.Parsers/Result/IParseResult.cs b/TASVideos.Parsers/Result/IParseResult.cs index d5447f45f..4bb5a56db 100644 --- a/TASVideos.Parsers/Result/IParseResult.cs +++ b/TASVideos.Parsers/Result/IParseResult.cs @@ -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. /// string? Annotations { get; } + + Dictionary Hashes { get; } +} + +public enum HashType +{ + Md5, Sha1, Sha256, Crc32 } diff --git a/TASVideos.Parsers/Result/SuccessResult.cs b/TASVideos.Parsers/Result/SuccessResult.cs index 1356d6feb..69b11fdf1 100644 --- a/TASVideos.Parsers/Result/SuccessResult.cs +++ b/TASVideos.Parsers/Result/SuccessResult.cs @@ -20,6 +20,8 @@ internal class SuccessResult(string fileExtension) : IParseResult public string? Annotations { get; internal set; } internal List WarningList { get; } = []; + + public Dictionary Hashes { get; } = []; } internal static class ParseResultExtensions diff --git a/tests/TASVideos.Core.Tests/Services/TestParseResult.cs b/tests/TASVideos.Core.Tests/Services/TestParseResult.cs index a70ba3286..4df3090e1 100644 --- a/tests/TASVideos.Core.Tests/Services/TestParseResult.cs +++ b/tests/TASVideos.Core.Tests/Services/TestParseResult.cs @@ -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 Hashes { get; init; } = new Dictionary(); } diff --git a/tests/TASVideos.Core.Tests/Services/UserFilesTests.cs b/tests/TASVideos.Core.Tests/Services/UserFilesTests.cs index afdc0eac4..01f0e9822 100644 --- a/tests/TASVideos.Core.Tests/Services/UserFilesTests.cs +++ b/tests/TASVideos.Core.Tests/Services/UserFilesTests.cs @@ -256,5 +256,6 @@ private class TestParseResult : IParseResult public double? FrameRateOverride => null; public long? CycleCount => null; public string? Annotations => null; + public Dictionary Hashes { get; init; } = new Dictionary(); } } diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2ParserTests.cs b/tests/TASVideos.MovieParsers.Tests/Bk2ParserTests.cs index c95310330..a15eccd82 100644 --- a/tests/TASVideos.MovieParsers.Tests/Bk2ParserTests.cs +++ b/tests/TASVideos.MovieParsers.Tests/Bk2ParserTests.cs @@ -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); + } } diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-md5.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-md5.bk2 new file mode 100644 index 000000000..0e1cb3ab0 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-md5.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-sha1.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-sha1.bk2 new file mode 100644 index 000000000..884db9f11 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-crc32-as-sha1.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5-as-sha1.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5-as-sha1.bk2 new file mode 100644 index 000000000..0bdd37329 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5-as-sha1.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5.bk2 new file mode 100644 index 000000000..e3717eab9 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-md5.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-missing.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-missing.bk2 new file mode 100644 index 000000000..9ee7d3f60 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-missing.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-na.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-na.bk2 new file mode 100644 index 000000000..4b3d6f222 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-na.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1-as-md5.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1-as-md5.bk2 new file mode 100644 index 000000000..5fff0d20f Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1-as-md5.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1.bk2 b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1.bk2 new file mode 100644 index 000000000..9a020aad3 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/Bk2SampleFiles/hash-sha1.bk2 differ diff --git a/tests/TASVideos.MovieParsers.Tests/Fm2ParserTests.cs b/tests/TASVideos.MovieParsers.Tests/Fm2ParserTests.cs index 6bad4daa2..1443e46b9 100644 --- a/tests/TASVideos.MovieParsers.Tests/Fm2ParserTests.cs +++ b/tests/TASVideos.MovieParsers.Tests/Fm2ParserTests.cs @@ -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); + } } diff --git a/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash-invalid.fm2 b/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash-invalid.fm2 new file mode 100644 index 000000000..3d6aa99bb --- /dev/null +++ b/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash-invalid.fm2 @@ -0,0 +1,2 @@ +romChecksum base64:ThisIsNotBase64 + diff --git a/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash-missing.fm2 b/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash-missing.fm2 new file mode 100644 index 000000000..e69de29bb diff --git a/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash.fm2 b/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash.fm2 new file mode 100644 index 000000000..c268b7ddb --- /dev/null +++ b/tests/TASVideos.MovieParsers.Tests/Fm2SampleFiles/hash.fm2 @@ -0,0 +1 @@ +romChecksum base64:6DgvglcLxhawvMAshDwbeQ== \ No newline at end of file diff --git a/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/hash.ltm b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/hash.ltm new file mode 100644 index 000000000..66be9c621 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/hash.ltm differ diff --git a/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/invalid-hash.ltm b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/invalid-hash.ltm new file mode 100644 index 000000000..655985d9d Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/invalid-hash.ltm differ diff --git a/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/missing-hash.ltm b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/missing-hash.ltm new file mode 100644 index 000000000..0621f2314 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/missing-hash.ltm differ diff --git a/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/no-hash.ltm b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/no-hash.ltm new file mode 100644 index 000000000..3e50c8900 Binary files /dev/null and b/tests/TASVideos.MovieParsers.Tests/LtmSampleFiles/no-hash.ltm differ diff --git a/tests/TASVideos.MovieParsers.Tests/LtmTests.cs b/tests/TASVideos.MovieParsers.Tests/LtmTests.cs index 531c5940d..7264557f2 100644 --- a/tests/TASVideos.MovieParsers.Tests/LtmTests.cs +++ b/tests/TASVideos.MovieParsers.Tests/LtmTests.cs @@ -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); + } }