// Copyright Epic Games, Inc. All Rights Reserved. using System; 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; #pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms #pragma warning disable CA1819 // Properties should not return arrays 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 { /// /// Length of an MD5 hash /// public const int LengthMD5 = 16; /// /// Length of a SHA1 hash /// public const int LengthSHA1 = 20; /// /// Retrieves an empty content hash /// public static ContentHash Empty { get; } = new ContentHash([]); /// /// The bytes compromising this hash /// public byte[] Bytes { get; private set; } /// /// Constructor /// /// The hash data public ContentHash(byte[] bytes) { 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 (other is 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 (a is null) { return b is 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 block of data, using a given algorithm. /// /// Data to compute the hash for /// Offset to the start of the data /// Length of the data /// Algorithm to use to create the hash /// New content hash instance containing the hash of the data public static ContentHash Compute(byte[] data, int offset, int count, HashAlgorithm algorithm) { return new ContentHash(algorithm.ComputeHash(data, offset, count)); } /// /// 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 block of data using MD5 /// /// Data to compute the hash for /// Offset to the start of the data /// Length of the data /// New content hash instance containing the hash of the data public static ContentHash MD5(byte[] data, int offset, int count) { using (MD5 algorithm = System.Security.Cryptography.MD5.Create()) { return Compute(data, offset, count, 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 (hash is 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); } } } }