// 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);
}
}
}
}