diff --git a/LiteDB/Client/Shared/SharedEngine.cs b/LiteDB/Client/Shared/SharedEngine.cs index 0e4cb72c5..1b7bb0b0c 100644 --- a/LiteDB/Client/Shared/SharedEngine.cs +++ b/LiteDB/Client/Shared/SharedEngine.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using LiteDB.Client.Shared; using LiteDB.Vector; namespace LiteDB @@ -18,7 +19,7 @@ public SharedEngine(EngineSettings settings) { _settings = settings; - var name = Path.GetFullPath(settings.Filename).ToLower().Sha1(); + var name = SharedMutexNameFactory.Create(settings.Filename, settings.SharedMutexNameStrategy); try { diff --git a/LiteDB/Client/Shared/SharedMutexNameFactory.cs b/LiteDB/Client/Shared/SharedMutexNameFactory.cs new file mode 100644 index 000000000..8f0198bb0 --- /dev/null +++ b/LiteDB/Client/Shared/SharedMutexNameFactory.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using LiteDB.Engine; + +namespace LiteDB.Client.Shared; + +internal static class SharedMutexNameFactory +{ + // Effective Windows limit for named mutexes (conservative). + private const int WINDOWS_MUTEX_NAME_MAX = 250; + + // If the caller adds "Global\" (7 chars) + the name + ".Mutex" (7 chars) to the mutex name, + // we account for it conservatively here without baking it into the return value. + // Adjust if your caller prepends something longer. + private const int CONSERVATIVE_EXTERNAL_PREFIX_LENGTH = 13; // e.g., "Global\\" + name + ".Mutex" + + internal static string Create(string fileName, SharedMutexNameStrategy strategy) + { + return strategy switch + { + SharedMutexNameStrategy.Default => CreateUsingUriEncodingWithFallback(fileName), + SharedMutexNameStrategy.UriEscape => CreateUsingUriEncoding(fileName), + SharedMutexNameStrategy.Sha1Hash => CreateUsingSha1(fileName), + _ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, null) + }; + } + + private static string CreateUsingUriEncodingWithFallback(string fileName) + { + var normalized = Normalize(fileName); + var uri = Uri.EscapeDataString(normalized); + + if (IsWindows() && + uri.Length + CONSERVATIVE_EXTERNAL_PREFIX_LENGTH > WINDOWS_MUTEX_NAME_MAX) + { + // Short, stable fallback well under the limit. + return "sha1-" + ComputeSha1Hex(normalized); + } + + return uri; + } + + private static string CreateUsingUriEncoding(string fileName) + { + var normalized = Normalize(fileName); + var uri = Uri.EscapeDataString(normalized); + + if (IsWindows() && + uri.Length + CONSERVATIVE_EXTERNAL_PREFIX_LENGTH > WINDOWS_MUTEX_NAME_MAX) + { + // Fallback to SHA to avoid ArgumentException on Windows. + return "sha1-" + ComputeSha1Hex(normalized); + } + + return uri; + } + + private static bool IsWindows() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + + internal static string CreateUsingSha1(string value) + { + var normalized = Normalize(value); + return ComputeSha1Hex(normalized); + } + + private static string Normalize(string path) + { + // Invariant casing + absolute path yields stable identity. + return Path.GetFullPath(path).ToLowerInvariant(); + } + + private static string ComputeSha1Hex(string input) + { + var data = Encoding.UTF8.GetBytes(input); + using var sha = SHA1.Create(); + var hashData = sha.ComputeHash(data); + + var sb = new StringBuilder(hashData.Length * 2); + foreach (var b in hashData) + { + sb.Append(b.ToString("X2")); + } + + return sb.ToString(); + } +} diff --git a/LiteDB/Engine/EngineSettings.cs b/LiteDB/Engine/EngineSettings.cs index e78034ab2..8400c7f63 100644 --- a/LiteDB/Engine/EngineSettings.cs +++ b/LiteDB/Engine/EngineSettings.cs @@ -71,6 +71,11 @@ public class EngineSettings /// Is used to transform a from the database on read. This can be used to upgrade data from older versions. /// public Func ReadTransform { get; set; } + + /// + /// Determines how the mutex name is generated. + /// + public SharedMutexNameStrategy SharedMutexNameStrategy { get; set; } /// /// Create new IStreamFactory for datafile diff --git a/LiteDB/Engine/SharedMutexNameStrategy.cs b/LiteDB/Engine/SharedMutexNameStrategy.cs new file mode 100644 index 000000000..4683264ec --- /dev/null +++ b/LiteDB/Engine/SharedMutexNameStrategy.cs @@ -0,0 +1,9 @@ + +namespace LiteDB.Engine; + +public enum SharedMutexNameStrategy +{ + Default, + UriEscape, + Sha1Hash +} \ No newline at end of file diff --git a/LiteDB/Utils/Extensions/StringExtensions.cs b/LiteDB/Utils/Extensions/StringExtensions.cs index 00ed9b54c..c34afebf8 100644 --- a/LiteDB/Utils/Extensions/StringExtensions.cs +++ b/LiteDB/Utils/Extensions/StringExtensions.cs @@ -37,24 +37,6 @@ public static bool IsWord(this string str) return true; } - public static string Sha1(this string value) - { - var data = Encoding.UTF8.GetBytes(value); - - using (var sha = SHA1.Create()) - { - var hashData = sha.ComputeHash(data); - var hash = new StringBuilder(); - - foreach (var b in hashData) - { - hash.Append(b.ToString("X2")); - } - - return hash.ToString(); - } - } - /// /// Implement SqlLike in C# string - based on /// https://stackoverflow.com/a/8583383/3286260