diff --git a/OpenRA.Game/CryptoUtil.cs b/OpenRA.Game/CryptoUtil.cs index 7cc40a3fc4..bb5a2ef8b4 100644 --- a/OpenRA.Game/CryptoUtil.cs +++ b/OpenRA.Game/CryptoUtil.cs @@ -22,6 +22,9 @@ namespace OpenRA // Fixed byte pattern for the OID header static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 }; + static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray(); + static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray(); + public static string PublicKeyFingerprint(RSAParameters parameters) { // Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes @@ -249,19 +252,44 @@ namespace OpenRA public static string SHA1Hash(Stream data) { - using (var csp = SHA1.Create()) - return new string(csp.ComputeHash(data).SelectMany(a => a.ToStringInvariant("x2")).ToArray()); + using var csp = SHA1.Create(); + return ToHex(csp.ComputeHash(data), true); } public static string SHA1Hash(byte[] data) { - using (var csp = SHA1.Create()) - return new string(csp.ComputeHash(data).SelectMany(a => a.ToStringInvariant("x2")).ToArray()); + using var csp = SHA1.Create(); + return ToHex(csp.ComputeHash(data), true); } public static string SHA1Hash(string data) { return SHA1Hash(Encoding.UTF8.GetBytes(data)); } + + public static string ToHex(ReadOnlySpan source, bool lowerCase = false) + { + if (source.Length == 0) + return string.Empty; + + // excessively avoid stack overflow if source is too large (considering that we're allocating a new string) + var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2]; + return ToHexInternal(source, buffer, lowerCase); + } + + static string ToHexInternal(ReadOnlySpan source, Span buffer, bool lowerCase) + { + var sourceIndex = 0; + var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet; + + for (var i = 0; i < buffer.Length; i += 2) + { + var b = source[sourceIndex++]; + buffer[i] = alphabet[b >> 4]; + buffer[i + 1] = alphabet[b & 0xF]; + } + + return new string(buffer); + } } } diff --git a/OpenRA.Game/Primitives/Color.cs b/OpenRA.Game/Primitives/Color.cs index 06c5cbca7c..5660386301 100644 --- a/OpenRA.Game/Primitives/Color.cs +++ b/OpenRA.Game/Primitives/Color.cs @@ -224,9 +224,9 @@ namespace OpenRA.Primitives public override string ToString() { if (A == 255) - return R.ToStringInvariant("X2") + G.ToStringInvariant("X2") + B.ToStringInvariant("X2"); + return CryptoUtil.ToHex(stackalloc byte[3] { R, G, B }); - return R.ToStringInvariant("X2") + G.ToStringInvariant("X2") + B.ToStringInvariant("X2") + A.ToStringInvariant("X2"); + return CryptoUtil.ToHex(stackalloc byte[4] { R, G, B, A }); } public static Color Transparent => FromArgb(0x00FFFFFF); diff --git a/OpenRA.Test/OpenRA.Game/Sha1Tests.cs b/OpenRA.Test/OpenRA.Game/Sha1Tests.cs new file mode 100644 index 0000000000..bdb811336a --- /dev/null +++ b/OpenRA.Test/OpenRA.Game/Sha1Tests.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using OpenRA.Primitives; + +namespace OpenRA.Test +{ + [TestFixture] + sealed class Sha1Tests + { + /// + /// https://en.wikipedia.org/wiki/SHA-1#Examples_and_pseudocode. + /// + /// The input string. + /// The expected hex string of the SHA1. + [TestCase("The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12")] + [TestCase("The quick brown fox jumps over the lazy cog", "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3")] + [TestCase("", "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + public void Sha1HexConvert(string input, string expected) + { + var actual = CryptoUtil.SHA1Hash(input); + + Assert.AreEqual(expected, actual); + } + + [TestCase(0xFF0000FF, "0000FF")] + [TestCase(0xFF00FFFF, "00FFFF")] + [TestCase(0xFFFF00FF, "FF00FF")] + [TestCase(0xAAFF00FF, "FF00FFAA")] + public void ColorsToHex(uint value, string expected) + { + var color = Color.FromArgb(value); + var actual = color.ToString(); + Assert.AreEqual(expected, actual); + } + } +}