From 56d33b5d62adc2e6d89132f1095ad8dba380753f Mon Sep 17 00:00:00 2001 From: Adrian Lewis Date: Fri, 1 Mar 2019 09:24:26 +0000 Subject: [PATCH 1/5] Refactored random block generation logic to avoid recreation of Random class. Fixed potential bug in DateTime usage - uses UtcNow instead of Now. --- NCuid/Cuid.cs | 181 +++++++++++++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 69 deletions(-) diff --git a/NCuid/Cuid.cs b/NCuid/Cuid.cs index 5d04d1a..d55741d 100644 --- a/NCuid/Cuid.cs +++ b/NCuid/Cuid.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Security; using System.Security.Cryptography; +using System.Text; namespace NCuid { @@ -12,13 +14,111 @@ namespace NCuid /// public static class Cuid { - private const int BlockSize = 4; - private const int Base = 36; + private abstract class RandomFragmentProvider + { + public abstract string GetBlock(int repeatCount); + + public abstract string GetFragment(int sliceLength); + } + + private class SimpleRandomFragmentProvider : RandomFragmentProvider + { + private readonly Random _randomGenerator = new Random(DateTime.UtcNow.Millisecond); + + public override string GetBlock(int repeatCount) + { + var sb = new StringBuilder(); + for (var index = 0; index < repeatCount; ++index) + { + sb.Append(SimpleRandomBlock(_randomGenerator)); + } + return sb.ToString(); + } + + public override string GetFragment(int sliceLength) + { + return SimpleRandomBlock(_randomGenerator).Slice(sliceLength); + } + + private static string SimpleRandomBlock(Random rnd) + { + var number = (long)(rnd.NextDouble() * DiscreteValues); + + var r = number.ToBase36().Pad(BlockSize); + + return r; + } + } + + private class SecureRandomFragmentProvider : RandomFragmentProvider + { + public override string GetBlock(int repeatCount) + { + using (var gen = new RNGCryptoServiceProvider()) + { + var sb = new StringBuilder(); + for (int index = 0; index < repeatCount; ++index) + { + sb.Append(SecureRandomBlock(gen)); + } + return sb.ToString(); + } + } + + public override string GetFragment(int sliceLength) + { + using (var gen = new RNGCryptoServiceProvider()) + { + return SecureRandomBlock(gen).Slice(sliceLength); + } + } + + private static string SecureRandomBlock(RandomNumberGenerator gen) + { + var data = new byte[8]; + gen.GetNonZeroBytes(data); + + var baseNum = ((double)BitConverter.ToUInt64(data, 0) / ulong.MaxValue); + var number = (long)(baseNum * DiscreteValues); + + return number.ToBase36().Pad(BlockSize); + } + } + + private static class RandomFragmentProviderFactory + { + private static readonly SimpleRandomFragmentProvider SimpleRandomFragmentProvider = new SimpleRandomFragmentProvider(); + private static readonly SecureRandomFragmentProvider SecureRandomFragmentProvider = new SecureRandomFragmentProvider(); + private static readonly IDictionary ProviderLookup; + + static RandomFragmentProviderFactory() + { + ProviderLookup = + new Dictionary + { + { RandomSource.Simple, SimpleRandomFragmentProvider }, + { RandomSource.Secure, SecureRandomFragmentProvider } + }; + } + + public static RandomFragmentProvider Get(RandomSource source) + { + if (!ProviderLookup.TryGetValue(source, out var provider)) + { + throw new IndexOutOfRangeException("Invalid RandomSource specified"); + } + + return provider; + } + } + + private const int BlockSize = 4; + private const int Base = 36; private static readonly ulong DiscreteValues = (ulong)Math.Pow(Base, BlockSize); private static ulong _globalCounter; private static string _hostname; - + private static string Hostname { get @@ -48,31 +148,10 @@ private static string Hostname /// A 25 characters string public static string Generate(RandomSource rs = RandomSource.Simple) { - var ts = DateTime.Now.ToUnixMilliTime().ToBase36(); + var ts = DateTime.UtcNow.ToUnixMilliTime().ToBase36(); var fingerprint = FingerPrint(); - string rnd; - - switch (rs) - { - case RandomSource.Secure: - { - using (var gen = new RNGCryptoServiceProvider()) - { - rnd = SecureRandomBlock(gen) + SecureRandomBlock(gen); - } - } - break; - case RandomSource.Simple: - { - var gen = new Random(); - rnd = SimpleRandomBlock(gen) + SimpleRandomBlock(gen); - } - break; - - default: - throw new IndexOutOfRangeException("Invalid RandomSource specified"); - } + var rnd = RandomFragmentProviderFactory.Get(rs).GetBlock(2); var counter = SafeCounter().ToBase36().Pad(BlockSize); @@ -86,27 +165,11 @@ public static string Generate(RandomSource rs = RandomSource.Simple) /// A 7 to 10 characters string (depending of the internal counter value) public static string Slug(RandomSource rs = RandomSource.Simple) { - var print = FingerPrint().Slice(0, 1) + FingerPrint().Slice(-1); + var print = FingerPrint().Slice(0, 1) + FingerPrint().Slice(-1); var counter = SafeCounter().ToBase36().Slice(-4); - var dt = DateTime.Now.ToUnixMilliTime().ToBase36(); - - string rnd; - - switch (rs) - { - case RandomSource.Secure: - using (var gen = new RNGCryptoServiceProvider()) - { - rnd = SecureRandomBlock(gen).Slice(-2); - } - break; - case RandomSource.Simple: - rnd = SimpleRandomBlock(new Random()).Slice(-2); - break; + var dt = DateTime.UtcNow.ToUnixMilliTime().ToBase36(); - default: - throw new IndexOutOfRangeException("Invalid RandomSource specified"); - } + var rnd = RandomFragmentProviderFactory.Get(rs).GetFragment(-2); return (dt.Slice(-2) + counter + print + rnd).ToLowerInvariant(); } @@ -121,9 +184,9 @@ public static string FingerPrint() { const int padding = 2; - var pid = Base36Converter.ToBase36((Process.GetCurrentProcess().Id)).Pad(padding); - var hostname = Hostname; - var length = hostname.Length; + var pid = Base36Converter.ToBase36((Process.GetCurrentProcess().Id)).Pad(padding); + var hostname = Hostname; + var length = hostname.Length; var inputNumber = hostname.Split().Aggregate(length + 36, (prev, c) => prev + c[0]); var hostId = Base36Converter.ToBase36(inputNumber).Pad(padding); @@ -135,30 +198,10 @@ private static ulong SafeCounter() _globalCounter = (_globalCounter < DiscreteValues) ? _globalCounter : 0; - + _globalCounter++; return _globalCounter - 1; } - - private static string SimpleRandomBlock(Random rnd) - { - var number = (long)(rnd.NextDouble() * DiscreteValues); - - var r = number.ToBase36().Pad(BlockSize); - - return r; - } - - private static string SecureRandomBlock(RandomNumberGenerator gen) - { - var data = new byte[8]; - gen.GetNonZeroBytes(data); - - var baseNum = ((double)BitConverter.ToUInt64(data, 0) / ulong.MaxValue); - var number = (long)(baseNum * DiscreteValues); - - return number.ToBase36().Pad(BlockSize); - } } } From 94c227550f1695df3fa4ce94554b0edab33acc4e Mon Sep 17 00:00:00 2001 From: Adrian Lewis Date: Fri, 1 Mar 2019 10:25:59 +0000 Subject: [PATCH 2/5] RandomSource.Simple is now thread-safe NCuid project supports .NET Standard 2.0 and .NET 4.6 --- NCuid.Tests/NCuid.Tests.csproj | 3 +- NCuid.sln | 11 +++-- NCuid/Cuid.cs | 17 ++++--- NCuid/NCuid.csproj | 83 +++------------------------------- 4 files changed, 26 insertions(+), 88 deletions(-) diff --git a/NCuid.Tests/NCuid.Tests.csproj b/NCuid.Tests/NCuid.Tests.csproj index 7775cda..4c90c02 100644 --- a/NCuid.Tests/NCuid.Tests.csproj +++ b/NCuid.Tests/NCuid.Tests.csproj @@ -8,7 +8,7 @@ Properties NCuid.Tests NCuid.Tests - v4.5 + v4.6 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -16,6 +16,7 @@ $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages False UnitTest + true diff --git a/NCuid.sln b/NCuid.sln index 88fd4b1..e7e9828 100644 --- a/NCuid.sln +++ b/NCuid.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.421 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NCuid.Tests", "NCuid.Tests\NCuid.Tests.csproj", "{1CD3A779-B529-497C-A186-F0637E316D2A}" EndProject @@ -27,12 +27,15 @@ Global {1CD3A779-B529-497C-A186-F0637E316D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9033C45-5859-4420-8D44-15A43BBC53EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9033C45-5859-4420-8D44-15A43BBC53EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release.Public|Any CPU.ActiveCfg = Release.Public|Any CPU - {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release.Public|Any CPU.Build.0 = Release.Public|Any CPU + {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release.Public|Any CPU.ActiveCfg = Release|Any CPU + {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release.Public|Any CPU.Build.0 = Release|Any CPU {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9033C45-5859-4420-8D44-15A43BBC53EA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C6CFFDFB-0411-46A4-B4A7-E309D553DA34} + EndGlobalSection EndGlobal diff --git a/NCuid/Cuid.cs b/NCuid/Cuid.cs index d55741d..61a92e8 100644 --- a/NCuid/Cuid.cs +++ b/NCuid/Cuid.cs @@ -6,6 +6,7 @@ using System.Security; using System.Security.Cryptography; using System.Text; +using System.Threading; namespace NCuid { @@ -23,26 +24,28 @@ private abstract class RandomFragmentProvider private class SimpleRandomFragmentProvider : RandomFragmentProvider { - private readonly Random _randomGenerator = new Random(DateTime.UtcNow.Millisecond); + private static readonly ThreadLocal RandomGenerator = new ThreadLocal(); public override string GetBlock(int repeatCount) { var sb = new StringBuilder(); + for (var index = 0; index < repeatCount; ++index) { - sb.Append(SimpleRandomBlock(_randomGenerator)); + sb.Append(SimpleRandomBlock()); } + return sb.ToString(); } public override string GetFragment(int sliceLength) { - return SimpleRandomBlock(_randomGenerator).Slice(sliceLength); + return SimpleRandomBlock().Slice(sliceLength); } - private static string SimpleRandomBlock(Random rnd) + private static string SimpleRandomBlock() { - var number = (long)(rnd.NextDouble() * DiscreteValues); + var number = (long)(RandomGenerator.Value.NextDouble() * DiscreteValues); var r = number.ToBase36().Pad(BlockSize); @@ -57,10 +60,12 @@ public override string GetBlock(int repeatCount) using (var gen = new RNGCryptoServiceProvider()) { var sb = new StringBuilder(); - for (int index = 0; index < repeatCount; ++index) + + for (var index = 0; index < repeatCount; ++index) { sb.Append(SecureRandomBlock(gen)); } + return sb.ToString(); } } diff --git a/NCuid/NCuid.csproj b/NCuid/NCuid.csproj index b42b15b..35de1e3 100644 --- a/NCuid/NCuid.csproj +++ b/NCuid/NCuid.csproj @@ -1,79 +1,8 @@ - - - + + - Debug - AnyCPU - {C9033C45-5859-4420-8D44-15A43BBC53EA} - Library - Properties - NCuid - NCuid - v4.5 - 512 + netstandard2.0;net46 + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\NCuid.XML - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - C:\Users\moonpyk\Documents\Clés\moonpyk_at_gmail_com.pfx - - - ..\nuget\lib\net40\ - TRACE - true - pdbonly - AnyCPU - prompt - MinimumRecommendedRules.ruleset - ..\nuget\lib\net40\NCuid.XML - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + From 86e9d7b3a77fc451ebc6f24fa63c9a50ff2863b1 Mon Sep 17 00:00:00 2001 From: Adrian Lewis Date: Fri, 1 Mar 2019 10:30:55 +0000 Subject: [PATCH 3/5] Simple random generator uses crypto-random number generator to provide the seed integer during construction of per-thread Random objects for better pseudo-randomness. --- NCuid/Cuid.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/NCuid/Cuid.cs b/NCuid/Cuid.cs index 61a92e8..9befe67 100644 --- a/NCuid/Cuid.cs +++ b/NCuid/Cuid.cs @@ -24,7 +24,17 @@ private abstract class RandomFragmentProvider private class SimpleRandomFragmentProvider : RandomFragmentProvider { - private static readonly ThreadLocal RandomGenerator = new ThreadLocal(); + private static readonly ThreadLocal RandomGenerator = + new ThreadLocal( + () => + { + using (var rng = new RNGCryptoServiceProvider()) + { + var block = new byte[4]; + rng.GetNonZeroBytes(block); + return new Random(BitConverter.ToInt32(block, 0)); + } + }); public override string GetBlock(int repeatCount) { From dfb2d179a610ca6a11a567f746fa5f17d61f2b17 Mon Sep 17 00:00:00 2001 From: Adrian Lewis Date: Fri, 1 Mar 2019 10:36:13 +0000 Subject: [PATCH 4/5] Assembly version with wildcard is not supported by .NET Core Reinstated version generation using MSBuild custom task --- .build/Version.targets | 2 +- NCuid/NCuid.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.build/Version.targets b/.build/Version.targets index f0f022c..f1303bc 100644 --- a/.build/Version.targets +++ b/.build/Version.targets @@ -1,7 +1,7 @@  - 1.0.0.* + 1.0.0.0 . diff --git a/NCuid/NCuid.csproj b/NCuid/NCuid.csproj index 35de1e3..64fa27e 100644 --- a/NCuid/NCuid.csproj +++ b/NCuid/NCuid.csproj @@ -4,5 +4,5 @@ netstandard2.0;net46 false - + From 49aec4a06ef3a3b0272d5b273c7ba6e9d2bcc7ca Mon Sep 17 00:00:00 2001 From: Adrian Lewis Date: Fri, 1 Mar 2019 10:51:45 +0000 Subject: [PATCH 5/5] Nuget package id revised to NCuid.Core --- NCuid/NCuid.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NCuid/NCuid.csproj b/NCuid/NCuid.csproj index 64fa27e..6aef70e 100644 --- a/NCuid/NCuid.csproj +++ b/NCuid/NCuid.csproj @@ -3,6 +3,10 @@ netstandard2.0;net46 false + true + NCuid.Core + MIT + https://github.com/dementeddevil/ncuid