فهرست منبع

Rewrite hex encoder/decoder

Bond_009 5 سال پیش
والد
کامیت
a245f5a0d4
32فایلهای تغییر یافته به همراه243 افزوده شده و 114 حذف شده
  1. 3 0
      .gitignore
  2. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  3. 1 6
      Emby.Drawing/Emby.Drawing.csproj
  4. 1 1
      Emby.Notifications/Emby.Notifications.csproj
  5. 1 1
      Emby.Photos/Emby.Photos.csproj
  6. 2 2
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  7. 2 2
      Emby.Server.Implementations/Library/UserManager.cs
  8. 4 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  9. 1 3
      Emby.Server.Implementations/Updates/InstallationManager.cs
  10. 1 1
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  11. 4 3
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  12. 1 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  13. 5 6
      MediaBrowser.Common/Cryptography/PasswordHash.cs
  14. 0 48
      MediaBrowser.Common/Extensions/CollectionExtensions.cs
  15. 26 0
      MediaBrowser.Common/Extensions/CopyToExtensions.cs
  16. 57 0
      MediaBrowser.Common/Hex.cs
  17. 0 24
      MediaBrowser.Common/HexHelper.cs
  18. 1 1
      MediaBrowser.Common/MediaBrowser.Common.csproj
  19. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  20. 1 1
      MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
  21. 1 1
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  22. 2 2
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  23. 1 1
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  24. 1 1
      MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
  25. 1 1
      Mono.Nat/Mono.Nat.csproj
  26. 1 1
      RSSDP/RSSDP.csproj
  27. 42 0
      benches/Jellyfin.Common.Benches/HexDecodeBenches.cs
  28. 29 0
      benches/Jellyfin.Common.Benches/HexEncodeBenches.cs
  29. 16 0
      benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj
  30. 14 0
      benches/Jellyfin.Common.Benches/Program.cs
  31. 19 0
      tests/Jellyfin.Common.Tests/HexTests.cs
  32. 3 3
      tests/Jellyfin.Common.Tests/PasswordHashTests.cs

+ 3 - 0
.gitignore

@@ -268,3 +268,6 @@ doc/
 # Deployment artifacts
 dist
 *.exe
+
+# BenchmarkDotNet artifacts
+BenchmarkDotNet.Artifacts

+ 1 - 1
Emby.Dlna/Emby.Dlna.csproj

@@ -12,7 +12,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 6
Emby.Drawing/Emby.Drawing.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -17,9 +17,4 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
-  <PropertyGroup>
-      <!-- We need at least C# 7.1 for the "default literal" feature-->
-    <LangVersion>latest</LangVersion>
-  </PropertyGroup>
-
 </Project>

+ 1 - 1
Emby.Notifications/Emby.Notifications.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
Emby.Photos/Emby.Photos.csproj

@@ -14,7 +14,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 2 - 2
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -2,11 +2,11 @@ using System;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Library
         {
             return string.IsNullOrEmpty(user.EasyPassword)
                 ? null
-                : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
+                : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
         }
 
         /// <summary>

+ 2 - 2
Emby.Server.Implementations/Library/UserManager.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Net;
@@ -31,7 +32,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 using Microsoft.Extensions.Logging;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -490,7 +490,7 @@ namespace Emby.Server.Implementations.Library
         {
             return string.IsNullOrEmpty(user.EasyPassword)
                 ? null
-                : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
+                : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
         }
 
         private void ResetInvalidLoginAttemptCount(User user)

+ 4 - 2
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -2304,8 +2304,10 @@ namespace Emby.Server.Implementations.LiveTv
             if (provider == null)
             {
                 throw new ResourceNotFoundException(
-                    string.Format("Couldn't find provider of type: '{0}'", info.Type)
-                );
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Couldn't find provider of type: '{0}'",
+                        info.Type));
             }
 
             await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);

+ 1 - 3
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net.Http;
@@ -19,7 +18,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Updates
 {
@@ -455,7 +453,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                var hash = ToHexString(md5.ComputeHash(stream));
+                var hash = Hex.Encode(md5.ComputeHash(stream));
                 if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase))
                 {
                     _logger.LogError(

+ 1 - 1
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 4 - 3
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Api.UserLibrary;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -25,7 +26,6 @@ using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Net.Http.Headers;
-using static MediaBrowser.Common.HexHelper;
 
 namespace MediaBrowser.Api.LiveTv
 {
@@ -887,8 +887,9 @@ namespace MediaBrowser.Api.LiveTv
         {
             // SchedulesDirect requires a SHA1 hash of the user's password
             // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
-            using (SHA1 sha = SHA1.Create()) {
-                return ToHexString(
+            using (SHA1 sha = SHA1.Create())
+            {
+                return Hex.Encode(
                     sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
             }
         }

+ 1 - 1
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 5 - 6
MediaBrowser.Common/Cryptography/PasswordHash.cs

@@ -4,7 +4,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using static MediaBrowser.Common.HexHelper;
 
 namespace MediaBrowser.Common.Cryptography
 {
@@ -102,13 +101,13 @@ namespace MediaBrowser.Common.Cryptography
             // Check if the string also contains a salt
             if (splitted.Length - index == 2)
             {
-                salt = FromHexString(splitted[index++]);
-                hash = FromHexString(splitted[index++]);
+                salt = Hex.Decode(splitted[index++]);
+                hash = Hex.Decode(splitted[index++]);
             }
             else
             {
                 salt = Array.Empty<byte>();
-                hash = FromHexString(splitted[index++]);
+                hash = Hex.Decode(splitted[index++]);
             }
 
             return new PasswordHash(id, hash, salt, parameters);
@@ -145,11 +144,11 @@ namespace MediaBrowser.Common.Cryptography
             if (Salt.Length != 0)
             {
                 str.Append('$')
-                    .Append(ToHexString(Salt));
+                    .Append(Hex.Encode(Salt, false));
             }
 
             return str.Append('$')
-                .Append(ToHexString(Hash)).ToString();
+                .Append(Hex.Encode(Hash, false)).ToString();
         }
     }
 }

+ 0 - 48
MediaBrowser.Common/Extensions/CollectionExtensions.cs

@@ -1,48 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Common.Extensions
-{
-    // The MS CollectionExtensions are only available in netcoreapp
-    public static class CollectionExtensions
-    {
-        public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
-        {
-            dictionary.TryGetValue(key, out var ret);
-            return ret;
-        }
-
-        /// <summary>
-        /// Copies all the elements of the current collection to the specified list
-        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
-        /// </summary>
-        /// <param name="source">The current collection that is the source of the elements.</param>
-        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
-        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
-        /// <typeparam name="T"></typeparam>
-        public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
-        {
-            for (int i = 0; i < source.Count; i++)
-            {
-                destination[index + i] = source[i];
-            }
-        }
-
-        /// <summary>
-        /// Copies all the elements of the current collection to the specified list
-        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
-        /// </summary>
-        /// <param name="source">The current collection that is the source of the elements.</param>
-        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
-        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
-        /// <typeparam name="T"></typeparam>
-        public static void CopyTo<T>(this IReadOnlyCollection<T> source, IList<T> destination, int index = 0)
-        {
-            foreach (T item in source)
-            {
-                destination[index++] = item;
-            }
-        }
-    }
-}

+ 26 - 0
MediaBrowser.Common/Extensions/CopyToExtensions.cs

@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
+    /// </summary>
+    public static class CollectionExtensions
+    {
+        /// <summary>
+        /// Copies all the elements of the current collection to the specified list
+        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
+        /// </summary>
+        /// <param name="source">The current collection that is the source of the elements.</param>
+        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
+        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
+        /// <typeparam name="T"></typeparam>
+        public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
+        {
+            for (int i = 0; i < source.Count; i++)
+            {
+                destination[index + i] = source[i];
+            }
+        }
+    }
+}

+ 57 - 0
MediaBrowser.Common/Hex.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Globalization;
+
+namespace MediaBrowser.Common
+{
+    /// <summary>
+    /// Encoding and decoding hex strings.
+    /// </summary>
+    public static class Hex
+    {
+        internal const string HexCharsLower = "0123456789abcdef";
+        internal const string HexCharsUpper = "0123456789ABCDEF";
+
+        /// <summary>
+        /// Encodes <c>bytes</c> as a hex string.
+        /// </summary>
+        /// <param name="bytes"></param>
+        /// <param name="lowercase"></param>
+        /// <returns><c>bytes</c> as a hex string.</returns>
+        public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true)
+        {
+            var hexChars = lowercase ? HexCharsLower : HexCharsUpper;
+
+            // TODO: use string.Create when it's supports spans
+            // Ref: https://github.com/dotnet/corefx/issues/29120
+            char[] s = new char[bytes.Length * 2];
+            int j = 0;
+            for (int i = 0; i < bytes.Length; i++)
+            {
+                s[j++] = hexChars[bytes[i] >> 4];
+                s[j++] = hexChars[bytes[i] & 0x0f];
+            }
+
+            return new string(s);
+        }
+
+        /// <summary>
+        /// Decodes a hex string into bytes.
+        /// </summary>
+        /// <param name="str">The <see cref="string" />.</param>
+        /// <returns>The decoded bytes.</returns>
+        public static byte[] Decode(ReadOnlySpan<char> str)
+        {
+            byte[] bytes = new byte[str.Length / 2];
+            int j = 0;
+            for (int i = 0; i < str.Length; i += 2)
+            {
+                bytes[j++] = byte.Parse(
+                    str.Slice(i, 2),
+                    NumberStyles.HexNumber,
+                    CultureInfo.InvariantCulture);
+            }
+
+            return bytes;
+        }
+    }
+}

+ 0 - 24
MediaBrowser.Common/HexHelper.cs

@@ -1,24 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-
-namespace MediaBrowser.Common
-{
-    public static class HexHelper
-    {
-        public static byte[] FromHexString(string str)
-        {
-            byte[] bytes = new byte[str.Length / 2];
-            for (int i = 0; i < str.Length; i += 2)
-            {
-                bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
-            }
-
-            return bytes;
-        }
-
-        public static string ToHexString(byte[] bytes)
-            => BitConverter.ToString(bytes).Replace("-", "");
-    }
-}

+ 1 - 1
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -21,7 +21,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -17,7 +17,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 2 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -19,7 +19,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
@@ -28,5 +28,5 @@
     <!-- We need at least C# 7.1 -->
     <LangVersion>latest</LangVersion>
   </PropertyGroup>
-   
+
 </Project>

+ 1 - 1
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -16,7 +16,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
Mono.Nat/Mono.Nat.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
 

+ 1 - 1
RSSDP/RSSDP.csproj

@@ -7,7 +7,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
 

+ 42 - 0
benches/Jellyfin.Common.Benches/HexDecodeBenches.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Globalization;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using MediaBrowser.Common;
+
+namespace Jellyfin.Common.Benches
+{
+    [MemoryDiagnoser]
+    public class HexDecodeBenches
+    {
+        private const int N = 1000000;
+        private readonly string data;
+
+        public HexDecodeBenches()
+        {
+            var tmp = new byte[N];
+            new Random(42).NextBytes(tmp);
+            data = Hex.Encode(tmp);
+        }
+
+        public static byte[] DecodeSubString(string str)
+        {
+            byte[] bytes = new byte[str.Length / 2];
+            for (int i = 0; i < str.Length; i += 2)
+            {
+                bytes[i / 2] = byte.Parse(
+                    str.Substring(i, 2),
+                    NumberStyles.HexNumber,
+                    CultureInfo.InvariantCulture);
+            }
+
+            return bytes;
+        }
+
+        [Benchmark]
+        public byte[] Decode() => Hex.Decode(data);
+
+        [Benchmark]
+        public byte[] DecodeSubString() => DecodeSubString(data);
+    }
+}

+ 29 - 0
benches/Jellyfin.Common.Benches/HexEncodeBenches.cs

@@ -0,0 +1,29 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using MediaBrowser.Common;
+
+namespace Jellyfin.Common.Benches
+{
+    [MemoryDiagnoser]
+    public class HexEncodeBenches
+    {
+        private const int N = 1000;
+        private readonly byte[] data;
+
+        public HexEncodeBenches()
+        {
+            data = new byte[N];
+            new Random(42).NextBytes(data);
+        }
+
+        [Benchmark]
+        public string HexEncode() => Hex.Encode(data);
+
+        [Benchmark]
+        public string BitConverterToString() => BitConverter.ToString(data);
+
+        [Benchmark]
+        public string BitConverterToStringWithReplace() => BitConverter.ToString(data).Replace("-", "");
+    }
+}

+ 16 - 0
benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+  </ItemGroup>
+
+</Project>

+ 14 - 0
benches/Jellyfin.Common.Benches/Program.cs

@@ -0,0 +1,14 @@
+using System;
+using BenchmarkDotNet.Running;
+
+namespace Jellyfin.Common.Benches
+{
+    public static class Program
+    {
+        public static void Main(string[] args)
+        {
+            _ = BenchmarkRunner.Run<HexEncodeBenches>();
+            _ = BenchmarkRunner.Run<HexDecodeBenches>();
+        }
+    }
+}

+ 19 - 0
tests/Jellyfin.Common.Tests/HexTests.cs

@@ -0,0 +1,19 @@
+using MediaBrowser.Common;
+using Xunit;
+
+namespace Jellyfin.Common.Tests
+{
+    public class HexTests
+    {
+        [Theory]
+        [InlineData("")]
+        [InlineData("00")]
+        [InlineData("01")]
+        [InlineData("000102030405060708090a0b0c0d0e0f")]
+        [InlineData("0123456789abcdef")]
+        public void RoundTripTest(string data)
+        {
+            Assert.Equal(data, Hex.Encode(Hex.Decode(data)));
+        }
+    }
+}

+ 3 - 3
tests/Jellyfin.Common.Tests/PasswordHashTests.cs

@@ -1,6 +1,6 @@
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using Xunit;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Jellyfin.Common.Tests
 {
@@ -15,8 +15,8 @@ namespace Jellyfin.Common.Tests
         {
             var pass = PasswordHash.Parse(passwordHash);
             Assert.Equal(id, pass.Id);
-            Assert.Equal(salt, ToHexString(pass.Salt));
-            Assert.Equal(hash, ToHexString(pass.Hash));
+            Assert.Equal(salt, Hex.Encode(pass.Salt, false));
+            Assert.Equal(hash, Hex.Encode(pass.Hash, false));
         }
 
         [Theory]