From 9da2f6e12795b945b34718fc3806e4660ae2ecb3 Mon Sep 17 00:00:00 2001 From: frederik-hoeft <43813142+frederik-hoeft@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:14:30 +0100 Subject: [PATCH] #33266: add Run support for UUIDv7 generation --- .../GUIDGeneratorTests.cs | 36 +++++++++++++++- .../InputParserTests.cs | 2 + .../Generators/GUID/GUIDGenerator.cs | 43 +++++++++++++++++++ .../Generators/GUID/GUIDRequest.cs | 35 ++++++++------- 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/GUIDGeneratorTests.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/GUIDGeneratorTests.cs index 7e3bffb7e557..a7daec617831 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/GUIDGeneratorTests.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/GUIDGeneratorTests.cs @@ -3,8 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; using System.Linq; - +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests @@ -56,6 +57,39 @@ public void GUIDv5Generator() Assert.AreEqual(0x5000, GetGUIDVersion(guid)); } + [TestMethod] + public void GUIDv7Generator() + { + var guidRequest = new GUID.GUIDRequest(7); + guidRequest.Compute(); + var guid = guidRequest.Result; + + Assert.IsNotNull(guid); + Assert.AreEqual(0x7000, GetGUIDVersion(guid)); + } + + [TestMethod] + public async Task GUIDv7GeneratorTimeOrderedAsync() + { + const int numberOfSamplesToCheck = 10; + ulong previousTimestampWithTrailingRandomData = 0uL; + for (int i = 0; i < numberOfSamplesToCheck; i++) + { + var guidRequest = new GUID.GUIDRequest(7); + guidRequest.Compute(); + var guid = guidRequest.Result; + + // can't hurt to assert invariants again + Assert.IsNotNull(guid); + Assert.AreEqual(0x7000, GetGUIDVersion(guid)); + ulong timestampWithTrailingRandomData = BinaryPrimitives.ReadUInt64BigEndian(guid.AsSpan()); + Assert.IsTrue(timestampWithTrailingRandomData > previousTimestampWithTrailingRandomData, "UUIDv7 wasn't time-ordered"); + + // ensure at least one millisecond passes for consistent time-ordering. we wait 10 ms just to be sure. + await Task.Delay(10); + } + } + [DataTestMethod] [DataRow(3, "ns:DNS", "abc", "5bd670ce-29c8-3369-a8a1-10ce44c7259e")] [DataRow(3, "ns:URL", "abc", "874a8cb4-4e91-3055-a476-3d3e2ffe375f")] diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/InputParserTests.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/InputParserTests.cs index 970cedfc1f84..f38aab45f117 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/InputParserTests.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests/InputParserTests.cs @@ -27,6 +27,8 @@ public class InputParserTests [DataRow("uUiD5 ns:URL abc", typeof(GUID.GUIDRequest))] [DataRow("Guidvv ns:DNS abc", null)] [DataRow("guidv4", typeof(GUID.GUIDRequest))] + [DataRow("guidv7", typeof(GUID.GUIDRequest))] + [DataRow("GUIDv7", typeof(GUID.GUIDRequest))] [DataRow("base64 abc", typeof(Base64.Base64Request))] [DataRow("base99 abc", null)] [DataRow("base64s abc", null)] diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDGenerator.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDGenerator.cs index e83649e3271d..491161fd36a1 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDGenerator.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Net; using System.Security.Cryptography; @@ -52,6 +53,48 @@ public static Guid V5(Guid uuidNamespace, string uuidName) return V3AndV5(uuidNamespace, uuidName, 5); } + public static Guid V7() + { + // A UUIDv7 looks like this (see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7) + // unix_ts_ms: + // 48-bit big-endian unsigned number of the Unix Epoch timestamp in milliseconds as per Section 6.1. Occupies bits 0 through 47 (octets 0-5). + // ver: + // The 4-bit version field as defined by Section 4.2, set to 0b0111 (7). Occupies bits 48 through 51 of octet 6. + // rand_a: + // 12 bits of pseudorandom data to provide uniqueness as per Section 6.9 and/or optional constructs to guarantee additional monotonicity as per Section 6.2. Occupies bits 52 through 63 (octets 6-7). + // var: + // The 2-bit variant field as defined by Section 4.1, set to 0b10. Occupies bits 64 and 65 of octet 8. + // rand_b: + // The final 62 bits of pseudorandom data to provide uniqueness as per Section 6.9 and/or an optional counter to guarantee additional monotonicity as per Section 6.2. Occupies bits 66 through 127 (octets 8-15). + byte[] result = new byte[16]; + Span buffer = result.AsSpan(); + + // first, fill the whole buffer with pseudorandom data, we use a CSPRNG for this because we don't know what users will use the generated values for. + RandomNumberGenerator.Fill(buffer); + + // then, get unix_ts_ms. we need to write in big-endian, so shift the 64 bit value by 16 to get the actual timestamp into the upper 48 bits. + ulong timestamp = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + ulong timestamp48 = timestamp << 16; + + // bytes 6 through 9 (0-indexed) need special treatment as they contain the version followed by 12 bits of randomness, followed by the variant field + // so we extract the existing random data and mask off the version and variant fields to get the correct format. + // for the initial read, endianness won't matter because it's pre-filled with random data anyways + uint bytes6To9 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[6..10]); + + // version field: set upper-most nibble (byte 6) to value 7: (clear by ANDing with 0, and set by ORing with 7). + // rand_a: remains 12 bit of unchanged random data (ANDing with 0xFFF, and ORing with 0x000) + // var: the upper two bits of byte 8 are set to 0b10: (clear upper two bits by ANDing with 0x3F, and set to 0b10 by ORing with 0x80) + // rand_b (partial): the rest of the data shall remain the unchanged pre-filled random data (ANDing with '1' and ORing with '0') + uint bytes6To9Masked = (bytes6To9 & 0x0FFF3FFF) | 0x70008000; + + // obviously we need to write the timestamp first. It contains 48 bit of data, followed by 16 bit of zeros (from the shift operation) + // therefore byte 6 and 7 will contains zeros after that first step. That's fine because we override that region with our masked-off version/variant data. + // make sure to write as big-endian here! + BinaryPrimitives.WriteUInt64BigEndian(buffer, timestamp48); + BinaryPrimitives.WriteUInt32BigEndian(buffer[6..], bytes6To9Masked); + return new Guid(result); + } + private static Guid V3AndV5(Guid uuidNamespace, string uuidName, short version) { byte[] namespaceBytes = uuidNamespace.ToByteArray(); diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDRequest.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDRequest.cs index dc4392a74499..60307ef77d01 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDRequest.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDRequest.cs @@ -4,7 +4,7 @@ using System; using System.Security.Cryptography; - +using WinRT; using Wox.Plugin.Logger; namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID @@ -42,6 +42,8 @@ public string Description return $"Version {Version} ({hashAlgorithm}): Namespace and name based GUID."; case 4: return "Version 4: Randomly generated GUID"; + case 7: + return "Version 7: Time-ordered randomly generated GUID"; default: return string.Empty; } @@ -60,20 +62,19 @@ public GUIDRequest(int version, string guidNamespace = null, string name = null) { Version = version; - if (Version < 1 || Version > 5 || Version == 2) + if (Version is < 1 or > 7 or 2 or 6) { throw new ArgumentException("Unsupported GUID version. Supported versions are 1, 3, 4 and 5"); } - if (version == 3 || version == 5) + if (version is 3 or 5) { if (guidNamespace == null) { throw new ArgumentNullException(null, NullNamespaceError); } - Guid guid; - if (GUIDGenerator.PredefinedNamespaces.TryGetValue(guidNamespace.ToLowerInvariant(), out guid)) + if (GUIDGenerator.PredefinedNamespaces.TryGetValue(guidNamespace.ToLowerInvariant(), out Guid guid)) { GuidNamespace = guid; } @@ -108,20 +109,18 @@ public bool Compute() IsSuccessful = true; try { - switch (Version) + Guid guid = Version switch { - case 1: - GuidResult = GUIDGenerator.V1(); - break; - case 3: - GuidResult = GUIDGenerator.V3(GuidNamespace.Value, GuidName); - break; - case 4: - GuidResult = GUIDGenerator.V4(); - break; - case 5: - GuidResult = GUIDGenerator.V5(GuidNamespace.Value, GuidName); - break; + 1 => GUIDGenerator.V1(), + 3 => GUIDGenerator.V3(GuidNamespace.Value, GuidName), + 4 => GUIDGenerator.V4(), + 5 => GUIDGenerator.V5(GuidNamespace.Value, GuidName), + 7 => GUIDGenerator.V7(), + _ => default, + }; + if (guid != default) + { + GuidResult = guid; } Result = GuidResult.ToByteArray();