Skip to content

Commit

Permalink
microsoft#33266: add Run support for UUIDv7 generation
Browse files Browse the repository at this point in the history
  • Loading branch information
frederik-hoeft committed Nov 4, 2024
1 parent da58b83 commit 9da2f6e
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

using System;
using System.Security.Cryptography;

using WinRT;
using Wox.Plugin.Logger;

namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 9da2f6e

Please sign in to comment.