// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace EpicGames.Core { /// /// Stores the hash value for a piece of content as a byte array, allowing it to be used as a dictionary key /// [JsonConverter(typeof(ContentHashJsonConverter))] public class ContentHash : IEquatable { public const int LengthMD5 = 16; public const int LengthSHA1 = 20; /// /// Retrieves an empty content hash /// public static ContentHash Empty { get; } = new ContentHash(Array.Empty()); /// /// The bytes compromising this hash /// public byte[] Bytes { get; private set; } /// /// Constructor /// /// The hash data public ContentHash(byte[] Bytes) { this.Bytes = Bytes; } /// /// Compares two content hashes for equality /// /// The object to compare against /// True if the hashes are equal, false otherwise public override bool Equals(object? Other) { return Equals(Other as ContentHash); } /// /// Compares two content hashes for equality /// /// The hash to compare against /// True if the hashes are equal, false otherwise public bool Equals(ContentHash? Other) { if (ReferenceEquals(Other, null)) { return false; } else { return Bytes.SequenceEqual(Other.Bytes); } } /// /// Compares two content hash objects for equality /// /// The first hash to compare /// The second has to compare /// True if the objects are equal, false otherwise public static bool operator ==(ContentHash? A, ContentHash? B) { if (ReferenceEquals(A, null)) { return ReferenceEquals(B, null); } else { return A.Equals(B); } } /// /// Compares two content hash objects for inequality /// /// The first hash to compare /// The second has to compare /// True if the objects are not equal, false otherwise public static bool operator !=(ContentHash? A, ContentHash? B) { return !(A == B); } /// /// Creates a content hash for a block of data, using a given algorithm. /// /// Data to compute the hash for /// Algorithm to use to create the hash /// New content hash instance containing the hash of the data public static ContentHash Compute(byte[] Data, HashAlgorithm Algorithm) { return new ContentHash(Algorithm.ComputeHash(Data)); } /// /// Creates a content hash for a string, using a given algorithm. /// /// Text to compute a hash for /// Algorithm to use to create the hash /// New content hash instance containing the hash of the text public static ContentHash Compute(string Text, HashAlgorithm Algorithm) { return new ContentHash(Algorithm.ComputeHash(Encoding.Unicode.GetBytes(Text))); } /// /// Creates a content hash for a file, using a given algorithm. /// /// File to compute a hash for /// Algorithm to use to create the hash /// New content hash instance containing the hash of the file public static ContentHash Compute(FileReference Location, HashAlgorithm Algorithm) { using (FileStream Stream = FileReference.Open(Location, FileMode.Open, FileAccess.Read, FileShare.Read)) { return new ContentHash(Algorithm.ComputeHash(Stream)); } } /// /// Creates a content hash for a block of data using MD5 /// /// Data to compute the hash for /// New content hash instance containing the hash of the data public static ContentHash MD5(byte[] Data) { using (MD5 Algorithm = System.Security.Cryptography.MD5.Create()) { return Compute(Data, Algorithm); } } /// /// Creates a content hash for a string using MD5. /// /// Text to compute a hash for /// New content hash instance containing the hash of the text public static ContentHash MD5(string Text) { using (MD5 Algorithm = System.Security.Cryptography.MD5.Create()) { return Compute(Text, Algorithm); } } /// /// Creates a content hash for a file, using a given algorithm. /// /// File to compute a hash for /// New content hash instance containing the hash of the file public static ContentHash MD5(FileReference Location) { using (MD5 Algorithm = System.Security.Cryptography.MD5.Create()) { return Compute(Location, Algorithm); } } /// /// Creates a content hash for a block of data using SHA1 /// /// Data to compute the hash for /// New content hash instance containing the hash of the data public static ContentHash SHA1(byte[] Data) { using (SHA1 Algorithm = System.Security.Cryptography.SHA1.Create()) { return Compute(Data, Algorithm); } } /// /// Creates a content hash for a string using SHA1. /// /// Text to compute a hash for /// New content hash instance containing the hash of the text public static ContentHash SHA1(string Text) { using (SHA1 Algorithm = System.Security.Cryptography.SHA1.Create()) { return Compute(Text, Algorithm); } } /// /// Creates a content hash for a file using SHA1. /// /// File to compute a hash for /// New content hash instance containing the hash of the file public static ContentHash SHA1(FileReference Location) { using (SHA1 Algorithm = System.Security.Cryptography.SHA1.Create()) { return Compute(Location, Algorithm); } } /// /// Parse a hash from a string /// /// Text to parse /// Value of the hash public static ContentHash Parse(string Text) { ContentHash? Hash; if (!TryParse(Text, out Hash)) { throw new ArgumentException(String.Format("'{0}' is not a valid content hash", Text)); } return Hash; } /// /// Parse a hash from a string /// /// Text to parse /// Value of the hash public static bool TryParse(string Text, [NotNullWhen(true)] out ContentHash? Hash) { byte[]? Bytes; if (StringUtils.TryParseHexString(Text, out Bytes)) { Hash = new ContentHash(Bytes); return true; } else { Hash = null; return false; } } /// /// Computes a hash code for this digest /// /// Integer value to use as a hash code public override int GetHashCode() { int HashCode = Bytes[0]; for (int Idx = 1; Idx < Bytes.Length; Idx++) { HashCode = (HashCode * 31) + Bytes[Idx]; } return HashCode; } /// /// Formats this hash as a string /// /// The hashed value public override string ToString() { return StringUtils.FormatHexString(Bytes); } } /// /// Converts values to and from JSON /// public class ContentHashJsonConverter : JsonConverter { /// public override ContentHash Read(ref Utf8JsonReader Reader, Type TypeToConvert, JsonSerializerOptions Options) { return ContentHash.Parse(Reader.GetString()); } /// public override void Write(Utf8JsonWriter Writer, ContentHash Value, JsonSerializerOptions Options) { Writer.WriteStringValue(Value.ToString()); } } /// /// Utility methods for serializing ContentHash objects /// public static class ContentHashExtensionMethods { /// /// Writes a ContentHash to a binary archive /// /// The writer to output data to /// The hash to write public static void WriteContentHash(this BinaryArchiveWriter Writer, ContentHash? Hash) { if (ReferenceEquals(Hash, null)) { Writer.WriteByteArray(null); } else { Writer.WriteByteArray(Hash.Bytes); } } /// /// Reads a ContentHash from a binary archive /// /// Reader to serialize data from /// New hash instance public static ContentHash? ReadContentHash(this BinaryArchiveReader Reader) { byte[]? Data = Reader.ReadByteArray(); if (Data == null) { return null; } else { return new ContentHash(Data); } } } }