diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5e20895 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = tab +tab_width = 4 + +dotnet_diagnostic.CS1998.severity = none + +# XML-styled files +[*.{csproj,props,targets}] +indent_size = 2 +indent_style = space +tab_width = 2 + +# NuGet config file +[nuget.config] +indent_size = 2 +indent_style = space +tab_width = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d4fca02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## 0.0.1 +* First release. +* Create Uuid v1 and v6. Convert between v1 and v6. +* Create Uuid v7 and v8MsSql. Convert between v7 and v8MsSql. +* Create Uuid v3 (MD5) and v5 (SHA1) +* Create Uuid v8SHA256 and v8SHA512 +* Get info about a Guid (dissect) +* Create monotonic sequence of v7 and v8MsSql Guid's +* Create NEWSEQUENTIALID (ms sql) +* Create/reverse Xor Guid +* Create/reverse numeric Guid +* Create/reverse incremented Guid + diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..d454b58 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..4610386 --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,14 @@ +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using GuidPhantom; + +namespace ConsoleApp1 +{ + internal class Program + { + public static void Main() + { + + } + } +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..20cc3c9 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,46 @@ + + + + README.md + Latest + 1701;1702;1591;1573 + true + + $(MSBuildThisFileDirectory) + $(RepoRootPath)bin\Packages\$(Configuration)\NuGet\ + + https://github.com/osexpert/GuidPhantom + + https://github.com/osexpert/GuidPhantom.git + git + + MIT + osexpert + See https://github.com/osexpert/GuidPhantom/blob/master/CHANGELOG.md for details + + 0.0.1 + GuidKit + Create Guid v1,v3,v5,v6,v7,v8MsSql,v8SHA256,v8SHA512,NEWSEQUENTIALID,XorGuid,NumericGuid,IncrementedGuid. Convert between v1 and v6. Convert between v7 and v8MsSql. Create monotonic sequence of v7 and v8MsSql Guid's. Get Guid info. + + true + + + + + + + + + + diff --git a/GuidPhantom.Tests/Class1.cs b/GuidPhantom.Tests/Class1.cs new file mode 100644 index 0000000..1f504f9 --- /dev/null +++ b/GuidPhantom.Tests/Class1.cs @@ -0,0 +1,295 @@ +using System.Linq; +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Data.SqlTypes; + +namespace GuidPhantom.Tests +{ + [TestClass] + public class UnitTest1 + { + public UnitTest1() + { + // warmup + TestV3(); + var h = GuidKit.CreateNEWSEQUENTIALID(); + Testv8sha512(); + } + + [TestMethod] + public void Ver7SequenceRising() + { + Guid? prev = null; + //int i = 0; + foreach (var h in GuidKit.CreateVersion7Sequence().Take(1000).ToList()) + { + if (prev != null && h.CompareTo(prev.Value) <= 0) + { + throw new Exception(h + " is less or equal to " + prev.Value); + } + prev = h; + } + } + + [TestMethod] + public void Ver7Sequence() + { + var s = GuidKit.CreateVersion7Sequence().Take(1).Single(); + Testv7(s); + } + [TestMethod] + public void Ver8MsSqlSequence() + { + var s = GuidKit.CreateVersion8MsSqlSequence().Take(1).Single(); + Testv8MsSql(s); + } + + [TestMethod] + public void Ver8MsSqlRising() + { + Guid? prev = null; + //int i = 0; + foreach (var h in GuidKit.CreateVersion8MsSqlSequence().Take(1000).ToList()) + { + //if (prev != null && h.ConvertVersion8MsSqlTo7().CompareTo(prev.Value.ConvertVersion8MsSqlTo7()) <= 0) + if (prev != null && new SqlGuid(h).CompareTo(new SqlGuid(prev.Value)) <= 0) + { + throw new Exception(h + " is less or equal to " + prev.Value); + } + + prev = h; + } + } + + [TestMethod] + public void NEWSEQUENTIALID_Rising() + { + Guid? prev = null; + //int i = 0; + for (int d = 0; d < 1000; d++) + { + var h = GuidKit.CreateNEWSEQUENTIALID(); + + if (prev != null && new SqlGuid(h).CompareTo(new SqlGuid(prev.Value)) <= 0) + { + throw new Exception(h + " is less or equal to " + prev.Value); + } + + prev = h; + } + } + + + [TestMethod] + public void TestV1() + { + var v1 = GuidKit.CreateVersion1(); + + Assert.IsTrue((GuidVariant.IETF, 1) == v1.GetVariantAndVersion()); + var inf1 = (GuidInfoVersion1And6)v1.GetGuidInfo(); + Assert.AreEqual(1, inf1.Version); + Assert.AreEqual(GuidVariant.IETF, inf1.Variant); + // should be pretty close, but strangely it is often over 1 seconds + var diff = (DateTimeOffset.UtcNow - inf1.Time); + Assert.IsTrue(diff.TotalSeconds < 2); + + Assert.AreEqual(v1, v1.ConvertVersion1To6().ConvertVersion6To1()); + + var v6 = v1.ConvertVersion1To6(); + Assert.IsTrue((GuidVariant.IETF, 6) == v6.GetVariantAndVersion()); + var inf6 = (GuidInfoVersion1And6)v6.GetGuidInfo(); + Assert.AreEqual(6, inf6.Version); + Assert.AreEqual(GuidVariant.IETF, inf6.Variant); + Assert.AreEqual(inf1.Timestamp, inf6.Timestamp); + Assert.AreEqual(inf1.Sequence, inf6.Sequence); + Assert.AreEqual(inf1.Node, inf6.Node); + } + + [TestMethod] + public void TestV6() + { + var v6 = GuidKit.CreateVersion6(); + + Assert.IsTrue((GuidVariant.IETF, 6) == v6.GetVariantAndVersion()); + var inf6 = (GuidInfoVersion1And6)v6.GetGuidInfo(); + Assert.AreEqual(6, inf6.Version); + Assert.AreEqual(GuidVariant.IETF, inf6.Variant); + // should be pretty close, but strangely it is often over 1 seconds + var diff = (DateTimeOffset.UtcNow - inf6.Time); + Assert.IsTrue(diff.TotalSeconds < 2); + + Assert.AreEqual(v6, v6.ConvertVersion6To1().ConvertVersion1To6()); + + var v1 = v6.ConvertVersion6To1(); + Assert.IsTrue((GuidVariant.IETF, 1) == v1.GetVariantAndVersion()); + var inf1 = (GuidInfoVersion1And6)v1.GetGuidInfo(); + Assert.AreEqual(1, inf1.Version); + Assert.AreEqual(GuidVariant.IETF, inf1.Variant); + Assert.AreEqual(inf6.Timestamp, inf1.Timestamp); + Assert.AreEqual(inf6.Sequence, inf1.Sequence); + Assert.AreEqual(inf6.Node, inf1.Node); + } + + + + [TestMethod] + public void TestV3() + { + // ref: https://www.uuidtools.com/generate/v3 + var v3 = new Guid("E11EAC0E-4D75-4567-BA60-683D357A9227").CreateVersion3("Test42"); + Assert.AreEqual(new Guid("0dd552e7-647f-3045-86f2-c006e1e17a89"), v3); + Assert.IsTrue((GuidVariant.IETF, 3) == v3.GetVariantAndVersion()); + var inf = (GuidInfoVersion)v3.GetGuidInfo(); + Assert.AreEqual(3, inf.Version); + Assert.AreEqual(GuidVariant.IETF, inf.Variant); + } + + + [TestMethod] + public void TestV4() + { + var v4 = GuidKit.CreateVersion4(); + Assert.IsTrue((GuidVariant.IETF, 4) == v4.GetVariantAndVersion()); + var inf = (GuidInfoVersion)v4.GetGuidInfo(); + Assert.AreEqual(4, inf.Version); + Assert.AreEqual(GuidVariant.IETF, inf.Variant); + } + + + + [TestMethod] + public void TestV5() + { + // ref: https://www.uuidtools.com/generate/v5 + var v5 = new Guid("E11EAC0E-4D75-4567-BA60-683D357A9227").CreateVersion5("Test42"); + Assert.AreEqual(new Guid("73cf5b24-114a-5a5b-837c-64cf22468258"), v5); + Assert.IsTrue((GuidVariant.IETF, 5) == v5.GetVariantAndVersion()); + var inf = (GuidInfoVersion)v5.GetGuidInfo(); + Assert.AreEqual(5, inf.Version); + Assert.AreEqual(GuidVariant.IETF, inf.Variant); + } + + [TestMethod] + public void Testv8sha256() + { + // ref: self + var v8 = new Guid("E11EAC0E-4D75-4567-BA60-683D357A9227").CreateVersion8SHA256("Test42"); + Assert.AreEqual(new Guid("306244bd-cd9e-88d1-a559-cf5a8b926d6c"), v8); + Assert.IsTrue((GuidVariant.IETF, 8) == v8.GetVariantAndVersion()); + var inf = (GuidInfoVersion)v8.GetGuidInfo(); + Assert.AreEqual(8, inf.Version); + Assert.AreEqual(GuidVariant.IETF, inf.Variant); + } + + [TestMethod] + public void Testv8sha512() + { + // ref: self + var v8 = new Guid("E11EAC0E-4D75-4567-BA60-683D357A9227").CreateVersion8SHA512("Test42"); + Assert.AreEqual(new Guid("f1ad1980-31b0-822e-8bd7-4d4c9892e5b6"), v8); + Assert.AreEqual((GuidVariant.IETF, (byte)8), v8.GetVariantAndVersion()); + var inf = (GuidInfoVersion)v8.GetGuidInfo(); + Assert.AreEqual(8, inf.Version); + Assert.AreEqual(GuidVariant.IETF, inf.Variant); + } + + [TestMethod] + public void TestNumeric() + { + var i0g = GuidKit.CreateNumericGuid(0); + var i42g = GuidKit.CreateNumericGuid(42); + var imaxg = GuidKit.CreateNumericGuid(int.MaxValue); + var i0 = GuidKit.ReverseNumericGuid(i0g); + var i42 = GuidKit.ReverseNumericGuid(i42g); + int imax = GuidKit.ReverseNumericGuid(imaxg); + Assert.AreEqual(0, i0); + Assert.AreEqual(42, i42); + Assert.AreEqual(int.MaxValue, imax); + } + + [TestMethod] + public void TestXor() + { + var v1 = GuidKit.CreateVersion1(); + var v7 = GuidKit.CreateVersion7(); + + var xor = GuidKit.CreateXorGuid(v1, v7); + Assert.AreEqual((GuidVariant.ApolloNCS, null), xor.GetVariantAndVersion()); + Assert.AreEqual(v7, GuidKit.ReverseXorGuid(xor, v1)); + Assert.AreEqual(v1, GuidKit.ReverseXorGuid(xor, v7)); + + } + + [TestMethod] + public void Testv7() + { + var v7 = GuidKit.CreateVersion7(); + Testv7(v7); + } + + + [TestMethod] + public void Testv8MsSql() + { + var v8 = GuidKit.CreateVersion8MsSql(); + Testv8MsSql(v8); + } + + public void Testv7(Guid v7) + { + Assert.AreEqual((GuidVariant.IETF, (byte)7), v7.GetVariantAndVersion()); + var inf7 = (GuidInfoVersion7And8MsSql)v7.GetGuidInfo(); + Assert.AreEqual(7, inf7.Version); + Assert.AreEqual(GuidVariant.IETF, inf7.Variant); + var diff = DateTimeOffset.UtcNow - inf7.Time; + Assert.IsTrue(diff.TotalSeconds < 2); + + Assert.AreEqual(v7, v7.ConvertVersion7To8MsSql().ConvertVersion8MsSqlTo7()); + + var v8 = v7.ConvertVersion7To8MsSql(); + Assert.AreEqual((GuidVariant.IETF, (byte)8), v8.GetVariantAndVersion()); + var inf8 = (GuidInfoVersion7And8MsSql)v8.GetGuidInfo(version8type: GuidVersion8Type.MsSql); + Assert.AreEqual(8, inf8.Version); + Assert.AreEqual(GuidVariant.IETF, inf8.Variant); + Assert.AreEqual(inf7.Timestamp, inf8.Timestamp); + } + + public void Testv8MsSql(Guid v8) + { + Assert.AreEqual((GuidVariant.IETF, (byte)8), v8.GetVariantAndVersion()); + var inf8 = (GuidInfoVersion7And8MsSql)v8.GetGuidInfo(version8type: GuidVersion8Type.MsSql); + Assert.AreEqual(8, inf8.Version); + Assert.AreEqual(GuidVariant.IETF, inf8.Variant); + var diff = DateTimeOffset.UtcNow - inf8.Time; + Assert.IsTrue(diff.TotalSeconds < 2); + + Assert.AreEqual(v8, v8.ConvertVersion8MsSqlTo7().ConvertVersion7To8MsSql()); + + var v7 = v8.ConvertVersion8MsSqlTo7(); + Assert.AreEqual((GuidVariant.IETF, (byte)7), v7.GetVariantAndVersion()); + var inf7 = (GuidInfoVersion7And8MsSql)v7.GetGuidInfo(); + Assert.AreEqual(7, inf7.Version); + Assert.AreEqual(GuidVariant.IETF, inf7.Variant); + Assert.AreEqual(inf8.Timestamp, inf7.Timestamp); + } + + [TestMethod] + public void TestIncremented() + { + // take the last 4 bytes of a Guid, increment them with increment, put the 4 bytes back. + // Depending on size of the increment, 1 to 4 bytes will be changed. + //var newG = Guid.CreateIncementedGuid(Guid seed, int increment) + + // Max 4 last bytes can be different. Based on how may of the last bytes are different (4-1), t + //var i = newG.ReverseIncementedGuid(seed); + + Guid base_g = Guid.NewGuid(); + Assert.AreEqual(int.MinValue, base_g.CreateIncrementedGuid(int.MinValue).ReverseIncrementedGuid(base_g)); + Assert.AreEqual(0, base_g.CreateIncrementedGuid(0).ReverseIncrementedGuid(base_g)); + Assert.AreEqual(42, base_g.CreateIncrementedGuid(42).ReverseIncrementedGuid(base_g)); + Assert.AreEqual(-420000, base_g.CreateIncrementedGuid(-420000).ReverseIncrementedGuid(base_g)); + Assert.AreEqual(int.MaxValue, base_g.CreateIncrementedGuid(int.MaxValue).ReverseIncrementedGuid(base_g)); + } + } +} + diff --git a/GuidPhantom.Tests/GuidPhantom.Tests.csproj b/GuidPhantom.Tests/GuidPhantom.Tests.csproj new file mode 100644 index 0000000..7b88e95 --- /dev/null +++ b/GuidPhantom.Tests/GuidPhantom.Tests.csproj @@ -0,0 +1,29 @@ + + + + net48;net6.0;net8.0 + enable + 9.0 + 1591 + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/GuidPhantom.sln b/GuidPhantom.sln new file mode 100644 index 0000000..f0885ae --- /dev/null +++ b/GuidPhantom.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34723.18 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{0B9FDDBD-8583-4335-9215-E13BE0D10B43}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GuidPhantom", "GuidPhantom\GuidPhantom.csproj", "{B7527D62-91EF-4021-B416-BABC0B669BA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GuidPhantom.Tests", "GuidPhantom.Tests\GuidPhantom.Tests.csproj", "{59FB39DE-01B5-419D-A9E7-284D9B67A9F2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0B9FDDBD-8583-4335-9215-E13BE0D10B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B9FDDBD-8583-4335-9215-E13BE0D10B43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B9FDDBD-8583-4335-9215-E13BE0D10B43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B9FDDBD-8583-4335-9215-E13BE0D10B43}.Release|Any CPU.Build.0 = Release|Any CPU + {B7527D62-91EF-4021-B416-BABC0B669BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7527D62-91EF-4021-B416-BABC0B669BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7527D62-91EF-4021-B416-BABC0B669BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7527D62-91EF-4021-B416-BABC0B669BA2}.Release|Any CPU.Build.0 = Release|Any CPU + {59FB39DE-01B5-419D-A9E7-284D9B67A9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59FB39DE-01B5-419D-A9E7-284D9B67A9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59FB39DE-01B5-419D-A9E7-284D9B67A9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59FB39DE-01B5-419D-A9E7-284D9B67A9F2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {73EAEE9F-CEA9-401E-A56E-8FA6023CB5D6} + EndGlobalSection +EndGlobal diff --git a/GuidPhantom/Class1.cs b/GuidPhantom/Class1.cs new file mode 100644 index 0000000..8b825bb --- /dev/null +++ b/GuidPhantom/Class1.cs @@ -0,0 +1,182 @@ +using System; +//using System.Buffers; +//using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; + +namespace GuidPhantom +{ + public enum GuidVariant : byte + { + /// + /// variant #0 is what was defined in the 1989 HP/Apollo Network Computing Architecture (NCA) + /// specification and implemented in NCS 1.x and DECrpc v1. + /// https://github.com/BeyondTrust/pbis-open/blob/master/dcerpc/uuid/uuid.c + /// NCS: (Apollo) Network Computing System. + /// + /// Guid.Empty will be of this variant. + /// https://segment.com/blog/a-brief-history-of-the-uuid/ + /// + ApolloNCS, + /// + /// Variant #1 is what was defined for the joint HP/DEC specification for the OSF + /// (in DEC's "UID Architecture Functional Specification Version X1.0.4") + /// and implemented in NCS 2.0, DECrpc v2, and OSF 1.0 DCE RPC. + /// https://github.com/BeyondTrust/pbis-open/blob/master/dcerpc/uuid/uuid.c + /// https://pubs.opengroup.org/onlinepubs/9629399/apdxa.htm + /// OSF: The Open Software Foundation (later: The Open Group). + /// NCS: (Apollo) Network Computing System. + /// DCE: (OSF) Distributed Computing Environment. + /// DEC: Digital Equipment Corporation (later: Digital) + /// + /// Later specified by IETF in RFC4122. + /// + IETF, + /// + /// Micsosoft used this variant at least for some OLE/RPC/COM/DCOM interface/CLSID's: + /// IUnknown interface ID: {00000000-0000-0000-C000-000000000046} + /// CLSID_StdOleLink {00000300-0000-0000-C000-000000000046} + /// coclass MsiPatch uuid (000c1094-0000-0000-c000-000000000046)] coclass MsiServerX3 + /// + Microsoft, + /// + /// Reserved. + /// Guid.AllBitsSet will be of this variant. + /// + Reserved, + } + + + + + + public enum GuidVersion8Type + { + Unknown, + MsSql, + } + + public class GuidInfo + { + public GuidInfo(GuidVariant variant) + { + Variant = variant; + } + + public GuidVariant Variant { get; } + } + + public class GuidInfoVersion : GuidInfo + { + public GuidInfoVersion(GuidVariant variant, byte version) : base(variant) + { + Version = version; + } + + public byte Version { get; } + } + + /// + /// Version 1 and 6 + /// + public class GuidInfoVersion1And6 : GuidInfoVersion + { + public GuidInfoVersion1And6(GuidVariant variant, byte version, long timestamp, short sequence, byte[] node) : base(variant, version) + { + if (node.Length != 6) + throw new ArgumentException("Node must be 6 bytes"); + + Timestamp = timestamp; + Sequence = sequence; + _node = node; + } + + /// + /// 60-bit timestamp, being the number of 100-nanosecond intervals since midnight 15 October 1582 Coordinated Universal Time (UTC) + /// + public long Timestamp { get; } + + public DateTimeOffset Time => GetTime(); + + private DateTimeOffset GetTime() + { + var start = new DateTimeOffset(1582, 10, 15, 0, 0, 0, TimeSpan.Zero); + // in old .net, AddMillis does not add fractional millis... + return start.AddTicks(Convert.ToInt64(Timestamp / 10000.0 * 10000)); + } + + /// + /// 14bits sequence + /// It can be: + /// - random data for every Guid generated + /// - a randomly seeded counter (at boot) that is incremented (with 1) every time: + /// a) the node changes + /// b) time is the same as for previously generated Guid + /// c) time goes back in time (compared to previously generated Guid) + /// Must fallback to randomly generated, if the counter is "lost". + /// + public short Sequence { get; } + + + byte[] _node; + + public string Node => $"{_node[0]:X2}:{_node[1]:X2}:{_node[2]:X2}:{_node[3]:X2}:{_node[4]:X2}:{_node[5]:X2}"; + + /// + /// 6 bytes mac address + /// + public byte[] GetNodeBytes() + { + var res = new byte[6]; + Array.Copy(_node, res, 6); + return res; + } + } + + /// + /// Version 7 (and Version 8 MsSql) + /// + public class GuidInfoVersion7And8MsSql : GuidInfoVersion + { + public GuidInfoVersion7And8MsSql(GuidVariant variant, byte version, long timestamp, short rand_a) : base(variant, version) + { + Timestamp = timestamp; + RandA = rand_a; + } + + /// + /// 48 bit Unix Epoch (UTC) timestamp in milliseconds + /// + public long Timestamp { get; } + + public DateTimeOffset Time => GetTime(); + + /// + /// rand_a can be one of: + /// - 12bits random data + /// - 12bits fractional milliseconds (OPTIONAL) + /// - 12bits monotonic sequence (randomly seeded) (OPTIONAL) + /// + public short RandA { get; } + + private DateTimeOffset GetTime()//bool add_rand_a_as_sub_milliseconds = false) + { + var t = DateTimeOffset.FromUnixTimeMilliseconds(Timestamp); + //if (false)//add_rand_a_as_sub_milliseconds) + //{ + // // in old .net, AddMillis does not add fractional millis... + // t = t.AddTicks(Convert.ToInt64(RandA / 4096.0 * 10000)); + //} + return t; + } + } +} diff --git a/GuidPhantom/GuidKit.cs b/GuidPhantom/GuidKit.cs new file mode 100644 index 0000000..4e62832 --- /dev/null +++ b/GuidPhantom/GuidKit.cs @@ -0,0 +1,1008 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace GuidPhantom +{ + public static class GuidKit + { + [DllImport("libuuid.so.1")] + private static extern int uuid_generate_time_safe(out byte[] bytes); + + [DllImport("rpcrt4.dll")] + private static extern int UuidCreateSequential(out Guid guid); + +#if NET8_0_OR_GREATER + // net8+ has it +#else + public static byte[] ToByteArray(this Guid g, bool bigEndian) + { + // if (_toByteArray.Value != null) +// return _toByteArray.Value(g, bigEndian); + + var bytes = g.ToByteArray(); + if (BitConverter.IsLittleEndian == bigEndian) + GuidKit.SwapEndian(bytes); + + return bytes; + } +#endif + + public static Guid CreateVersion1() => CreateVersion1(out var _); + + //static Delegate CreateInstanceMethodDelegate(MethodInfo method) + //{ + // var instExp = Expression.Parameter(method.DeclaringType); + // var instExpArr = new[] { instExp }; + // var paramsExpArr = method.GetParameters() + // .Select(p => Expression.Parameter(p.ParameterType, p.Name)) + // .ToArray(); + // var call = Expression.Call(instExp, method, paramsExpArr); + // return Expression.Lambda(call, instExpArr.Concat(paramsExpArr)).Compile(); + //} + + //static readonly Lazy?> _toByteArray = new(() => + //{ + // var meth = typeof(Guid).GetMethod("ToByteArray", new Type[] { typeof(bool) }); + // if (meth != null) + // { + // return (Func)CreateInstanceMethodDelegate(meth); + // } + // return null; + //}); + + + //static readonly Lazy?> _createVersion7 = new(() => + //{ + // var meth = typeof(Guid).GetMethod("CreateVersion7", new Type[] { }); + // if (meth != null) + // { + // return (Func)meth.CreateDelegate(typeof(Func)); + // } + // return null; + //}); + + //static readonly Lazy?> _createVersion7_dto = new(() => + //{ + // var meth = typeof(Guid).GetMethod("CreateVersion7", new Type[] { typeof(DateTimeOffset) }); + // if (meth != null) + // { + // return (Func)meth.CreateDelegate(typeof(Func)); + // } + // return null; + //}); + + /// + /// Create version1 Guid: time + sequence + node/mac + /// + /// true if generated safely (will be globally unique). false is generated unsafely (only locally unique) + /// Version 1 Guid + /// + public static Guid CreateVersion1(out bool safe) + { + // If true, will throw is Guid is not generated safely (eg. not with a valid mac address, not with a reliable sequence etc.) + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + const int RPC_S_UUID_LOCAL_ONLY = 1824; + const int RPC_S_UUID_NO_ADDRESS = 1739; + + int res = UuidCreateSequential(out var g); + + if (res == 0) + { + safe = true; + return g; + } + + if (res == RPC_S_UUID_LOCAL_ONLY || res == RPC_S_UUID_NO_ADDRESS) + { + safe = false; + return g; + } + + throw new Exception("Error: " + res); + } + else + { + var bytes = new byte[16]; + var res = uuid_generate_time_safe(out bytes); + if (res == 0) + { + safe = true; + return FromByteArray(bytes, bigEndian: true); + } + + if (res == -1) + { + safe = false; + return FromByteArray(bytes, bigEndian: true); + } + + throw new Exception("Error: " + res); + } + } + + /// + /// Create version 6 (same as v1, but bits rearranged to make it ordered) + /// + /// + public static Guid CreateVersion6() => CreateVersion6(out var _); + + /// + /// Create version 6 (same as v1, but bits rearranged to make it ordered) + /// + /// true if generated safely (will be globally unique). false is generated unsafely (only locally unique) + /// + public static Guid CreateVersion6(out bool safe) + { + var v1 = CreateVersion1(out safe); + return v1.SwapVersion1And6(verify_version: 1); + } + + /// + /// MS SQL SERVER: NEWSEQUENTIALID + /// NEWSEQUENTIALID is Version1 with swapped endianess. This means this Guid is non-standard and the variant/version will be random/invalid. + /// Non-standard. + /// + /// NEWSEQUENTIALID + public static Guid CreateNEWSEQUENTIALID() => CreateNEWSEQUENTIALID(out var _); + + + /// + /// MS SQL SERVER: NEWSEQUENTIALID + /// NEWSEQUENTIALID is Version1 with swapped endianess. This means this Guid is non-standard and the variant/version will be random/invalid. + /// Non-standard. + /// + /// NEWSEQUENTIALID + public static Guid CreateNEWSEQUENTIALID(out bool safe) + { + var v1 = CreateVersion1(out safe); + var bytes = v1.ToByteArray(bigEndian: true); + SwapEndian(bytes); + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// Version is only defined for variant 1, so for other variants, Version is returned as NULL. + /// This to prevent using Version for logic when there is none. + /// + /// + /// Variant and version + public static (GuidVariant Variant, byte? Version) GetVariantAndVersion(this Guid g) + { + var b = g.ToByteArray(bigEndian: true); + return GetVariantAndVersion(b); + } + + private static (GuidVariant Variant, byte? Version) GetVariantAndVersion(byte[] b) + { + GuidVariant? variant; + + //byte[8]: + //0xx Apollo + //10x IETF + //110 MS + //111 reserved + + // order is important! + if ((b[8] & 0b1000_0000) == 0) + variant = GuidVariant.ApolloNCS; + else if ((b[8] & 0b0100_0000) == 0) + { + // because the above check we know highest bit is 1 + variant = GuidVariant.IETF; + } + else if ((b[8] & 0b0010_0000) == 0) + { + // because the above checks we know higher 2 bits are 1 + variant = GuidVariant.Microsoft; + } + else + { + // because the above checks we know higher 3 bits are 1 + variant = GuidVariant.Reserved; + } + + byte? version = null; + + if (variant == GuidVariant.IETF) + version = (byte)((b[6] & 0b1111_0000) >> 4); + + return (variant.Value, version); + } + + private static void SwapEndian(byte[] guid) + { + SwapBytes(guid, 0, 3); + SwapBytes(guid, 1, 2); + SwapBytes(guid, 4, 5); + SwapBytes(guid, 6, 7); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapBytes(byte[] guid, int a, int b) + { + var temp = guid[a]; + guid[a] = guid[b]; + guid[b] = temp; + } + + + + +#if NET8_0_OR_GREATER + // already has it (one that takes ReadOnlySpan) + + /// + /// Create Guid from byte array + /// + /// + /// + /// Guid + public static Guid FromByteArray(byte[] bytes, bool bigEndian) => new Guid(bytes, bigEndian); +#else + /// + /// Create Guid from byte array + /// + /// + /// + /// Guid + public static Guid FromByteArray(byte[] bytes, bool bigEndian) + { + if (bytes.Length != 16) + { + throw new ArgumentException("Must be 16 bytes"); + } + + if (BitConverter.IsLittleEndian == bigEndian) + SwapEndian(bytes); + + return new Guid(bytes); + } +#endif + + /// + /// Guid.NewGuid() + /// + /// Guid.NewGuid() + public static Guid CreateVersion4() => Guid.NewGuid(); + + /// + /// Create version7 Guid. 6 first bytes is unix timestamp in milliseconds, the rest is random data. + /// Rand_a is not used as counter, only random data. This means that if more than 1 Guid is made on the same millisecond, + /// their relative order is random. This implementation matches the one in .NET 9. + /// + /// Version 7 Guid + public static Guid CreateVersion7() + { + // if (_createVersion7.Value != null) + // return _createVersion7.Value(); + + return CreateVersion7(DateTimeOffset.UtcNow); + } + + + //#if NET9_0_OR_GREATER + + // public Guid CreateVersion7(DateTimeOffset timestamp) => Guid.CreateVersion7(timestamp); + //#else + + /// + /// Create version7 Guid. 6 first bytes is unix timestamp in milliseconds, the rest is random data. + /// Rand_a is not used as counter, only random data. This means that if more than 1 Guid is made on the same millisecond, + /// their relative order is random. This implementation matches the one in .NET 9. + /// + /// + /// Version 7 Guid + /// + public static Guid CreateVersion7(DateTimeOffset timestamp) + { + // if (_createVersion7_dto.Value != null) + // return _createVersion7_dto.Value(timestamp); + + // 2^48 is roughly 8925.5 years, which from the Unix Epoch means we won't + // overflow until around July of 10,895. So there isn't any need to handle + // it given that DateTimeOffset.MaxValue is December 31, 9999. However, we + // can't represent timestamps prior to the Unix Epoch since UUIDv7 explicitly + // stores a 48-bit unsigned value, so we do need to throw if one is passed in. + + long unix_ts_ms = timestamp.ToUnixTimeMilliseconds(); + if (unix_ts_ms < 0) + throw new ArgumentOutOfRangeException(nameof(timestamp)); + + short dummy = 0; + return CreateVersion7(unix_ts_ms, ref dummy, false); + } + + private static Guid CreateVersion7(long unix_ts_ms, ref short sequence, bool setSequence) + { + if (unix_ts_ms < 0) + throw new ArgumentOutOfRangeException(nameof(unix_ts_ms)); + + // This isn't the most optimal way, but we don't have an easy way to get + // secure random bytes in corelib without doing this since the secure rng + // is in a different layer. + Guid result = Guid.NewGuid(); + + var bytes = result.ToByteArray(bigEndian: true); + + if ((bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("Not variant " + GuidVariant.IETF); + + // time + bytes[0] = (byte)(unix_ts_ms >> (5 * 8)); + bytes[1] = (byte)(unix_ts_ms >> (4 * 8)); + bytes[2] = (byte)(unix_ts_ms >> (3 * 8)); + bytes[3] = (byte)(unix_ts_ms >> (2 * 8)); + bytes[4] = (byte)(unix_ts_ms >> (1 * 8)); + bytes[5] = (byte)unix_ts_ms; + + // set ver 7 + const byte newVer = 7; + bytes[6] = (byte)((newVer << 4) | (bytes[6] & 0b0000_1111)); + + // sequence + if (setSequence) + { + if (sequence < 0 || sequence > 4095) + throw new ArgumentException("Sequence must be between 0 and 4095"); + + bytes[6] = (byte)((bytes[6] & 0b1111_0000) | (sequence >> 8) & 0b0000_1111); + bytes[7] = (byte)sequence; + } + else + { + short currSeq = (short)((bytes[6] & 0b0000_1111) << 8 | bytes[7]); + sequence = currSeq; + } + + return FromByteArray(bytes, bigEndian: true); + } + //#endif + + /// + /// Create monotonic (always increasing) sequence of v7 Guid's. + /// When more than one Guid is created per millisecond, 12bit rand_a (initially seeded by random data) is used as counter (+1). + /// When counter rollover (rand_a > 4095), the timestamp is incremented (+1). + /// + /// + /// Version7 sequence + public static IEnumerable CreateVersion7Sequence(TimeProvider timeProvider) => CreateVersion7Sequence(TimeProvider.System); + + /// + /// Create monotonic (always increasing) sequence of v7 Guid's. + /// When more than one Guid is created per millisecond, 12bit rand_a (initially seeded by random data) is used as counter (+1). + /// When counter rollover (rand_a > 4095), the timestamp is incremented (+1). + /// + /// Version7 sequence> + public static IEnumerable CreateVersion7Sequence() => CreateVersion7Or8MsSqlSequence(TimeProvider.System, 7); + + /// + /// Same as CreateVersion7Sequence, but data is rearranged to a v8 variant that is ordered in MsSql. + /// + /// Version8MsSql sequence + public static IEnumerable CreateVersion8MsSqlSequence() => CreateVersion7Or8MsSqlSequence(TimeProvider.System, 8); + + /// + /// Same as CreateVersion7Sequence but bits rearranged to make it ordered in MsSql (and then set as Version8) + /// + /// + /// Version8MsSql sequence + public static IEnumerable CreateVersion8MsSqlSequence(TimeProvider timeProvider) => CreateVersion7Or8MsSqlSequence(timeProvider, 8); + + private static IEnumerable CreateVersion7Or8MsSqlSequence(TimeProvider timeProvider, byte version) + { + long? prev_ts = null; + short sequence = 0; + + while (true) + { + long now_ts = timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); + + bool setSequence = false; + if (now_ts <= prev_ts) + { + now_ts = prev_ts.Value; + sequence++; + + if (sequence > 4095) + { + now_ts++; + } + else + { + setSequence = true; + } + } + + if (version == 7) + yield return CreateVersion7(now_ts, ref sequence, setSequence); + else if (version == 8) + yield return CreateVersion8MsSql(now_ts, ref sequence, setSequence); + else + throw new InvalidOperationException("Not ver 7 or 8"); + + prev_ts = now_ts; + } + } + + /// + /// SHA256 hash of namespace and name + /// + /// + /// + /// Version8SHA256 Guid + public static Guid CreateVersion8SHA256(this Guid namespaceId, string name) + { + return CreateNamespaceGuid(namespaceId, () => SHA256.Create(), Encoding.UTF8.GetBytes(name), 8); + } + + /// + /// SHA512 hash of namespace and name + /// + /// + /// + /// Version8SHA512 Guid + public static Guid CreateVersion8SHA512(this Guid namespaceId, string name) + { + return CreateNamespaceGuid(namespaceId, () => SHA512.Create(), Encoding.UTF8.GetBytes(name), 8); + } + + /// + /// Same as Version7 but bits rearranged to make it ordered in MsSql (and then set as Version8) + /// + /// Version8MsSql Guid + public static Guid CreateVersion8MsSql() => CreateVersion8MsSql(DateTimeOffset.Now); + + /// + /// Same as Version7 but bits rearranged to make it ordered in MsSql (and then set as Version8) + /// + /// + /// Version8MsSql Guid + /// + public static Guid CreateVersion8MsSql(DateTimeOffset timestamp) + { + // if (_createVersion7_dto.Value != null) + // return _createVersion7_dto.Value(timestamp); + + // 2^48 is roughly 8925.5 years, which from the Unix Epoch means we won't + // overflow until around July of 10,895. So there isn't any need to handle + // it given that DateTimeOffset.MaxValue is December 31, 9999. However, we + // can't represent timestamps prior to the Unix Epoch since UUIDv7 explicitly + // stores a 48-bit unsigned value, so we do need to throw if one is passed in. + + long unix_ts_ms = timestamp.ToUnixTimeMilliseconds(); + if (unix_ts_ms < 0) + throw new ArgumentOutOfRangeException(nameof(timestamp)); + + short dummy = 0; + return CreateVersion8MsSql(unix_ts_ms, ref dummy, false); + } + + /// + /// Create a Guid where the 6 last bytes is the unix time in milliseconds (v7 has time in the 6 first bytes) + /// + /// This will make them sort correctly in ms sql server, that has a weird/opposite way of sorting: + /// https://stackoverflow.com/questions/7810602/sql-server-guid-sort-algorithm-why + /// "More technically, we look at bytes {10 to 15} first, then {8-9}, then {6-7}, then {4-5}, and lastly {0 to 3}." + /// + /// + /// Version8MsSql Guid + /// + /// + private static Guid CreateVersion8MsSql(long unix_ts_ms, ref short sequence, bool setSequence) + { + if (unix_ts_ms < 0) + throw new ArgumentOutOfRangeException(nameof(unix_ts_ms)); + + // This isn't the most optimal way, but we don't have an easy way to get + // secure random bytes in corelib without doing this since the secure rng + // is in a different layer. + Guid result = Guid.NewGuid(); + + var bytes = result.ToByteArray(bigEndian: true); + + if ((bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("Not variant " + GuidVariant.IETF); + + // time + bytes[10] = (byte)(unix_ts_ms >> (5 * 8)); + bytes[11] = (byte)(unix_ts_ms >> (4 * 8)); + bytes[12] = (byte)(unix_ts_ms >> (3 * 8)); + bytes[13] = (byte)(unix_ts_ms >> (2 * 8)); + bytes[14] = (byte)(unix_ts_ms >> (1 * 8)); + bytes[15] = (byte)unix_ts_ms; + + // set ver 8 + const byte newVer = 8; + bytes[6] = (byte)((newVer << 4) | (bytes[6] & 0b0000_1111)); + + // sequence + if (setSequence) + { + if (sequence < 0 || sequence > 4095) + throw new ArgumentException("Sequence must be between 0 and 4095"); + + bytes[8] = (byte)((bytes[8] & 0b1100_0000) | (sequence >> 6) & 0b0011_1111); + bytes[9] = (byte)((sequence << 2) & 0b1111_1100); + } + else + { + short currSeq = (short)((bytes[8] & 0b0011_1111) << 6 | (bytes[9] & 0b1111_1100) >> 2); + sequence = currSeq; + } + + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// MD5 hash of namepace and name + /// + /// + /// + /// Version 3 Guid + [SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Per spec.")] + [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms", Justification = "Per spec.")] + public static Guid CreateVersion3(this Guid namespaceId, string name) + { + return CreateNamespaceGuid(namespaceId, () => MD5.Create(), Encoding.UTF8.GetBytes(name), 3); + } + + /// + /// SHA1 hash of namespace and name + /// + /// + /// + /// Version 5 Guid + [SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Per spec.")] + [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms", Justification = "Per spec.")] + public static Guid CreateVersion5(this Guid namespaceId, string name) + { + return CreateNamespaceGuid(namespaceId, () => SHA1.Create(), Encoding.UTF8.GetBytes(name), 5); + } + + /// + /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. + /// Based on: https://github.com/Faithlife/FaithlifeUtility/blob/master/src/Faithlife.Utility/GuidUtility.cs + /// + /// The ID of the namespace. + /// The name (within that namespace). + /// The version number of the UUID to create; this value must be either + /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). + /// A UUID derived from the namespace and name. + [SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Per spec.")] + [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms", Justification = "Per spec.")] + private static Guid CreateNamespaceGuid(Guid namespaceId, Func hashAlgo, byte[] nameBytes, byte version) + { + if (!(version == 3 || version == 5 || version == 8)) + throw new ArgumentOutOfRangeException(nameof(version), "Version must be either 3 or 5 (or 8)."); + + var namespaceBytes = namespaceId.ToByteArray(bigEndian: true); + + byte[] data = new byte[namespaceBytes.Length + nameBytes.Length]; + namespaceBytes.CopyTo(data, 0); + nameBytes.CopyTo(data, namespaceBytes.Length); + + // compute the hash of the namespace ID concatenated with the name data + //var data = namespaceBytes.Concat(nameBytes).ToArray(); + + byte[] hash; + using (var algorithm = hashAlgo()) + hash = algorithm.ComputeHash(data); + + // Copy 16 first bytes from hash straight into the guid (ignoring the rest of the bytes in the hash) + var newGuid = new byte[16]; + Array.Copy(hash, 0, newGuid, 0, 16); + + // set version + newGuid[6] = (byte)((version << 4) | (newGuid[6] & 0b0000_1111)); + + // set variant IETF + newGuid[8] = (byte)((newGuid[8] & 0b0011_1111) | 0b1000_0000); + + return FromByteArray(newGuid, bigEndian: true); + } + + /// + /// Create xor Guid from 2 regular Guid's. + /// Since a and b must be variant IETF, the result will always be a Guid of variant ApolloNCS. + /// Can only be used safely once, so will fail is a or b is already xor'ed. + /// Non-standard. + /// + /// + /// + /// Xor Guid + /// + public static Guid CreateXorGuid(this Guid a, Guid b) + { + var a_bytes = a.ToByteArray(bigEndian: true); + var b_bytes = b.ToByteArray(bigEndian: true); + + if ((a_bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("a is not variant " + GuidVariant.IETF); + if ((b_bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("b is not variant " + GuidVariant.IETF); + + byte[] bytes = new byte[16]; + for (int i = 0; i < 16; i++) + bytes[i] = (byte)(a_bytes[i] ^ b_bytes[i]); + + // Sanity + if ((bytes[8] & 0b1100_0000) != 0b0000_0000) + throw new InvalidOperationException("Result is not variant " + GuidVariant.ApolloNCS); + + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// From the xor-Guid and a or b, will return the opposite (xorGuid + a = b or xorGuid + b = a). + /// + /// + /// + /// Opposite of a_or_b. If a_or_b is a, b is returned. If a_or_b is b, a is returned. + /// + public static Guid ReverseXorGuid(this Guid xorGuid, Guid a_or_b) + { + var xor_bytes = xorGuid.ToByteArray(bigEndian: true); + var a_or_b_bytes = a_or_b.ToByteArray(bigEndian: true); + + if ((xor_bytes[8] & 0b1100_0000) != 0b0000_0000) + throw new InvalidOperationException("xorGuid is not variant " + GuidVariant.ApolloNCS); + if ((a_or_b_bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("a_or_b is not variant " + GuidVariant.IETF); + + byte[] bytes = new byte[16]; + for (int i = 0; i < 16; i++) + bytes[i] = (byte)(xor_bytes[i] ^ a_or_b_bytes[i]); + + // Sanity + if ((bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("Result is not variant " + GuidVariant.IETF); + + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// Swap the millis time part between the 6 first bytes and 6 last bytes. + /// + /// + /// + /// + /// + private static Guid SwapVersion7And8MsSql(this Guid g, byte verify_version)//, bool swap_rand_a = true) + { + // Swap the 12 bits (OPTIONAL sub milliseconds) in octets 6-7 + + var bytes = g.ToByteArray(bigEndian: true); + SwapVersion7And8MsSql(bytes, verify_version);//, swap_rand_a); + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// Convert from v7 to v8MsSql + /// + /// + /// A version 8MsSql Guid + public static Guid ConvertVersion7To8MsSql(this Guid g_v7) => SwapVersion7And8MsSql(g_v7, verify_version: 7); + + /// + /// Convert from v8MsSql to v7 + /// + /// + /// A version 7 Guid + public static Guid ConvertVersion8MsSqlTo7(this Guid g_v8mssql) => SwapVersion7And8MsSql(g_v8mssql, verify_version: 8); + + /// + /// Convert from v1 to v6 + /// + /// + /// A version 6 Guid + public static Guid ConvertVersion1To6(this Guid g_v1) => SwapVersion1And6(g_v1, verify_version: 1); + + /// + /// Convert from v6 to v1 + /// + /// + /// A version 1 Guid + public static Guid ConvertVersion6To1(this Guid g_v6) => SwapVersion1And6(g_v6, verify_version: 6); + + private static void SwapVersion7And8MsSql(byte[] bytes, byte verify_version)//, bool swap_rand_a = true) + { + //byte[8]: + //0xx Apollo + //10x IETF + //110 MS + //111 reserved + if ((bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("Not variant " + GuidVariant.IETF); + + byte oldVer = (byte)((bytes[6] & 0b1111_0000) >> 4); + if (oldVer != verify_version) + throw new InvalidOperationException($"Version mismatch: expected {verify_version}, was {oldVer}"); + byte newVer; + if (oldVer == 7) + newVer = 8; + else if (oldVer == 8) + newVer = 7; + else + throw new InvalidOperationException("Not version 7 or 8"); + + // time + SwapBytes(bytes, 0, 10); + SwapBytes(bytes, 1, 11); + SwapBytes(bytes, 2, 12); + SwapBytes(bytes, 3, 13); + SwapBytes(bytes, 4, 14); + SwapBytes(bytes, 5, 15); + + //if (true)//swap_rand_a) + { + var bytes_6 = bytes[6]; + var bytes_7 = bytes[7]; + var bytes_8 = bytes[8]; + var bytes_9 = bytes[9]; + + // The sub-second part: + // swap 12 bits from byte 7 - 8 <-> byte 9 - 10.these 12 bits are at different locations. + // bit1 - 4: version 7 / 8 | bit5 - 8: byte9.bit3 - 6(4bits) + bytes[6] = (byte)(newVer << 4 | (bytes_8 >> 2) & 0b0000_1111); + // bit1 - 2: byte9.bit7 - 8 | bit3 - 8: byte10.bit1 - 6 + bytes[7] = (byte)((bytes_8 << 6) & 0b1100_0000 | (bytes_9 >> 2) & 0b0011_1111); + // bit1 - 2: preserve variant(byte9.bit1-2) | bit3 - 6: byte7.bit5 - 8 | bit7 - 8: byte8.bit1 - 2 + bytes[8] = (byte)(bytes_8 & 0b1100_0000 | (bytes_6 << 2) & 0b0011_1100 | (bytes_7 >> 6) & 0b0000_0011); + // bit1 - 6: byte8.bit3 - 8 | bit7 - 8: byte10.bit7 - 8(preserve) + bytes[9] = (byte)((bytes_7 << 2) & 0b1111_1100 | bytes_9 & 0b000_0011); + } + //else + //{ + // // set version + // bytes[6] = (byte)((newVer << 4) | (bytes[6] & 0b0000_1111)); + //} + + + } + + /// + /// Fake a Guid with digits 0-9 (no hex). + /// Range:
+ /// 0 -> 00000000-0000-0000-0000-00000000000
+ /// 1 -> 00000000-0000-0000-0000-00000000001
+ /// 10 -> 00000000-0000-0000-0000-00000000010
+ /// 42 -> 00000000-0000-0000-0000-00000000042
+ /// ...
+ /// 2147483647 -> 00000000-0000-0000-0000-02147483647 (int.MaxValue)
+ /// Non-standard. + ///
+ /// + /// A numeric Guid + public static Guid CreateNumericGuid(int i) + { + if (i < 0) + throw new ArgumentException("Must be positive"); + return new Guid(i.ToString().PadLeft(32, '0')); + } + + + /// + /// 00000000-0000-0000-0000-00000000000 -> 0
+ /// 00000000-0000-0000-0000-00000000001 -> 1
+ /// 00000000-0000-0000-0000-00000000010 -> 10
+ /// 00000000-0000-0000-0000-00000000042 -> 42
+ /// ...
+ /// 00000000-0000-0000-0000-02147483647 (int.MaxValue) -> 2147483647
+ /// + /// Everything else will give error + ///
+ /// + /// The number used when creating the numeric Guid + public static int ReverseNumericGuid(this Guid g) + { + return int.Parse(g.ToString("N")); + } + + private static Guid SwapVersion1And6(this Guid g, byte verify_version) + { + var bytes = g.ToByteArray(bigEndian: true); + SwapVersion1And6(bytes, verify_version); + return FromByteArray(bytes, bigEndian: true); + } + + private static void SwapVersion1And6(byte[] bytes, byte verify_version) + { + //byte[8]: + //0xx Apollo + //10x IETF + //110 MS + //111 reserved + if ((bytes[8] & 0b1100_0000) != 0b1000_0000) + throw new InvalidOperationException("Not variant " + GuidVariant.IETF); + + var bytes_0 = bytes[0]; + var bytes_1 = bytes[1]; + var bytes_2 = bytes[2]; + var bytes_3 = bytes[3]; + var bytes_4 = bytes[4]; + var bytes_5 = bytes[5]; + var bytes_6 = bytes[6]; + var bytes_7 = bytes[7]; + + byte oldVer = (byte)((bytes[6] & 0b1111_0000) >> 4); + if (oldVer != verify_version) + throw new InvalidOperationException($"Version mismatch: expected {verify_version}, was {oldVer}"); + byte newVer; + if (oldVer == 1) + { + newVer = 6; + + bytes[0] = (byte)((bytes_6 & 0b0000_1111) << 4 | (bytes_7 & 0b1111_0000) >> 4); + bytes[1] = (byte)((bytes_7 & 0b0000_1111) << 4 | (bytes_4 & 0b1111_0000) >> 4); + bytes[2] = (byte)((bytes_4 & 0b0000_1111) << 4 | (bytes_5 & 0b1111_0000) >> 4); + bytes[3] = (byte)((bytes_5 & 0b0000_1111) << 4 | (bytes_0 & 0b1111_0000) >> 4); + + bytes[4] = (byte)((bytes_0 & 0b0000_1111) << 4 | (bytes_1 & 0b1111_0000) >> 4); + bytes[5] = (byte)((bytes_1 & 0b0000_1111) << 4 | (bytes_2 & 0b1111_0000) >> 4); + + bytes[6] = (byte)(newVer << 4 | bytes_2 & 0b0000_1111); + bytes[7] = bytes_3; + } + else if (oldVer == 6) + { + newVer = 1; + + bytes[6] = (byte)(newVer << 4 | (bytes_0 & 0b1111_0000) >> 4); + bytes[7] = (byte)((bytes_0 & 0b0000_1111) << 4 | (bytes_1 & 0b1111_0000) >> 4); + + bytes[4] = (byte)((bytes_1 & 0b0000_1111) << 4 | (bytes_2 & 0b1111_0000) >> 4); + bytes[5] = (byte)((bytes_2 & 0b0000_1111) << 4 | (bytes_3 & 0b1111_0000) >> 4); + + bytes[0] = (byte)((bytes_3 & 0b0000_1111) << 4 | (bytes_4 & 0b1111_0000) >> 4); + bytes[1] = (byte)((bytes_4 & 0b0000_1111) << 4 | (bytes_5 & 0b1111_0000) >> 4); + bytes[2] = (byte)((bytes_5 & 0b0000_1111) << 4 | bytes_6 & 0b0000_1111); + bytes[3] = bytes_7; + } + else + throw new InvalidOperationException("Not version 1 or 6"); + } + + /// + /// Get information about a Guid + /// + /// + /// + /// Info about the Guid + /// + public static GuidInfo GetGuidInfo(this Guid g, GuidVersion8Type version8type = GuidVersion8Type.Unknown) + { + var b = g.ToByteArray(bigEndian: true); + + var vv = GetVariantAndVersion(b); + if (vv.Variant == GuidVariant.IETF) + { + if (vv.Version == 1 || vv.Version == 6) + { + if (vv.Version == 1) + { + SwapVersion1And6(b, verify_version: 1); + } + + // v6 + long time = (long)b[0] << 52 | + (long)b[1] << 44 | + (long)b[2] << 36 | + (long)b[3] << 28 | + (long)b[4] << 20 | + (long)b[5] << 12 | + (long)(b[6] & 0b0000_1111) << 8 | + (long)b[7]; + + short seq = (short)( + (b[8] & 0b0011_1111) << 8 | + b[9] + ); + + var mac = new byte[6]; + Array.Copy(b, 10, mac, 0, 6); + + return new GuidInfoVersion1And6(vv.Variant, vv.Version.Value, time, seq, mac); + } + else if (vv.Version == 7 || (vv.Version == 8 && version8type == GuidVersion8Type.MsSql)) + { + if ((vv.Version == 8 && version8type == GuidVersion8Type.MsSql)) + { + SwapVersion7And8MsSql(b, verify_version: 8); + } + + // v7 + long time = (long)b[0] << (5 * 8) | + (long)b[1] << (4 * 8) | + (long)b[2] << (3 * 8) | + (long)b[3] << (2 * 8) | + (long)b[4] << (1 * 8) | + (long)b[5]; + + short seq = (short)( + (b[6] & 0b0000_1111) << 8 | + b[7] + ); + + return new GuidInfoVersion7And8MsSql(vv.Variant, vv.Version.Value, time, seq); + } + else if (vv.Version is not null) + return new GuidInfoVersion(vv.Variant, vv.Version.Value); + else + throw new Exception($"Impossible: {GuidVariant.IETF} always have version"); + } + else + return new GuidInfo(vv.Variant); + } + + /// + /// Increment a Guid. Depending on size of the increment, 0 to 4 last bytes will be changed/incremented. + /// Use case: "hide" (encode) an int inside an existing Guid. + /// Non-standard. + /// + /// + /// Can be negative + /// Incremented Guid + public static Guid CreateIncrementedGuid(this Guid g_base, int increment) + { + var bytes = g_base.ToByteArray(bigEndian: true); + + int i = + bytes[12] << (3 * 8) | + bytes[13] << (2 * 8) | + bytes[14] << (1 * 8) | + bytes[15]; + + i += increment; + + bytes[12] = (byte)(i >> (3 * 8)); + bytes[13] = (byte)(i >> (2 * 8)); + bytes[14] = (byte)(i >> (1 * 8)); + bytes[15] = (byte)i; + + return FromByteArray(bytes, bigEndian: true); + } + + /// + /// From the base Guid and the incremented Guid, get back the increment used to create the incremented Guid. + /// + /// + /// + /// The increment used when creating the incremented Guid + /// + public static int ReverseIncrementedGuid(this Guid g_incremented, Guid g_base) + { + var base_bytes = g_base.ToByteArray(bigEndian: true); + var inc_bytes = g_incremented.ToByteArray(bigEndian: true); + + // bytes should be equal until the 4-0 last bytes + for (int i = 0; i <= 15; i++) + { + var eq = base_bytes[i] == inc_bytes[i]; + if (i <= 11 && !eq) + throw new ArgumentException("First 12 bytes should be equal"); + + if (i >= 12 && !eq) + { + int base_int = 0; + int inc_int = 0; + for (int j = i; j <= 15; j++) + { + base_int |= base_bytes[j] << ((15 - j) * 8); + inc_int |= inc_bytes[j] << ((15 - j) * 8); + } + return inc_int - base_int; + } + } + + // both guid's equal + return 0; + } + } +} diff --git a/GuidPhantom/GuidPhantom.csproj b/GuidPhantom/GuidPhantom.csproj new file mode 100644 index 0000000..b3af439 --- /dev/null +++ b/GuidPhantom/GuidPhantom.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0;net8.0 + GuidPhantom + 9.0 + enable + + + + + + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..70976d2 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.*" + } +} \ No newline at end of file diff --git a/keypair.key b/keypair.key new file mode 100644 index 0000000..b961bed Binary files /dev/null and b/keypair.key differ diff --git a/nuget push.bat b/nuget push.bat new file mode 100644 index 0000000..460da57 --- /dev/null +++ b/nuget push.bat @@ -0,0 +1,4 @@ +REM set myKey= +set ver=0.0.1 + +nuget push bin\Packages\Release\NuGet\GuidPhantom.%ver%.nupkg -src https://api.nuget.org/v3/index.json -ApiKey %myKey% diff --git a/pack release.bat b/pack release.bat new file mode 100644 index 0000000..7705bb8 --- /dev/null +++ b/pack release.bat @@ -0,0 +1 @@ +dotnet pack -c release \ No newline at end of file diff --git a/public.key b/public.key new file mode 100644 index 0000000..b1da31c Binary files /dev/null and b/public.key differ