Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PTRun] add support for UUIDv7 generation #35757

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Value Generator Plugin

The Value Generator plugin is used to generate hashes for strings, to calculate base64 encodings, escape and encode URLs/URIs and to generate GUIDs versions 1, 3, 4 and 5.
The Value Generator plugin is used to generate hashes for strings, to calculate base64 encodings, escape and encode URLs/URIs and to generate GUIDs of version 1, 3, 4, 5, and 7.

![Image of Value Generator plugin](/doc/images/launcher/plugin/community.valuegenerator.png)

Expand Down Expand Up @@ -34,7 +34,10 @@ The Value Generator plugin is used to generate hashes for strings, to calculate

### [`GUIDGenerator`](/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Generators/GUID/GUIDGenerator.cs)
- Utility class for generating or calculating GUIDs
- Generating GUID versions 1 and 4 is done using builtin APIs. [`UuidCreateSequential`](https://learn.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-uuidcreatesequential) for version 1 and `System.Guid.NewGuid()` for version 4
- Generating GUID versions 1, 4, and 7 is done using builtin APIs:
- [`UuidCreateSequential`](https://learn.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-uuidcreatesequential) for version 1
- `System.Guid.NewGuid()` for version 4
- `System.Guid.CreateVersion7()` for version 7
- Versions 3 and 5 take two parameters, a namespace and a name
- The namespace must be a valid GUID or one of the [predefined ones](https://datatracker.ietf.org/doc/html/rfc4122#appendix-C)
- The `PredefinedNamespaces` dictionary contains aliases for the predefined namespaces
Expand Down
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;
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 void GUIDv7GeneratorTimeOrdered()
{
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.
Thread.Sleep(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 @@ -12,7 +12,7 @@
namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests
{
[TestClass]
public class InputParserTests
public partial class InputParserTests
{
[DataTestMethod]
[DataRow("md5 abc", typeof(Hashing.HashRequest))]
Expand All @@ -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 Expand Up @@ -90,25 +92,22 @@ public void ParserTest(string input, Type? expectedRequestType, bool expectExcep

private static bool CommandIsKnown(string command)
{
string[] hashes = new string[] { "md5", "sha1", "sha256", "sha384", "sha512", "base64", "base64d" };
string[] hashes = ["md5", "sha1", "sha256", "sha384", "sha512", "base64", "base64d"];
if (hashes.Contains(command.ToLowerInvariant()))
{
return true;
}

Regex regexGuiUUID = new Regex("^(guid|uuid)([1345]{0,1}|v[1345]{1})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (regexGuiUUID.IsMatch(command))
if (GetKnownUuidImplementations().IsMatch(command))
{
return true;
}

string[] uriCommands = new string[] { "url", "urld", "esc:hex", "uesc:hex", "esc:data", "uesc:data" };
if (uriCommands.Contains(command.ToLowerInvariant()))
{
return true;
}

return false;
string[] uriCommands = ["url", "urld", "esc:hex", "uesc:hex", "esc:data", "uesc:data"];
return uriCommands.Contains(command.ToLowerInvariant());
}

[GeneratedRegex("^(guid|uuid)([13457]{0,1}|v[13457]{1})$", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")]
private static partial Regex GetKnownUuidImplementations();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public static Guid V5(Guid uuidNamespace, string uuidName)
return V3AndV5(uuidNamespace, uuidName, 5);
}

public static Guid V7()
{
return Guid.CreateVersion7();
}

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,6 @@

using System;
using System.Security.Cryptography;

using Wox.Plugin.Logger;

namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID
Expand All @@ -19,34 +18,15 @@ public class GUIDRequest : IComputeRequest

private int Version { get; set; }

public string Description
public string Description => Version switch
{
get
{
switch (Version)
{
case 1:
return "Version 1: Time base GUID";
case 3:
case 5:
string hashAlgorithm;
if (Version == 3)
{
hashAlgorithm = HashAlgorithmName.MD5.ToString();
}
else
{
hashAlgorithm = HashAlgorithmName.SHA1.ToString();
}

return $"Version {Version} ({hashAlgorithm}): Namespace and name based GUID.";
case 4:
return "Version 4: Randomly generated GUID";
default:
return string.Empty;
}
}
}
1 => "Version 1: Time base GUID",
3 => $"Version 3 ({HashAlgorithmName.MD5}): Namespace and name based GUID.",
4 => "Version 4: Randomly generated GUID",
5 => $"Version 5 ({HashAlgorithmName.SHA1}): Namespace and name based GUID.",
7 => "Version 7: Time-ordered randomly generated GUID",
_ => string.Empty,
};

private Guid? GuidNamespace { get; set; }

Expand All @@ -60,20 +40,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");
throw new ArgumentException("Unsupported GUID version. Supported versions are 1, 3, 4, 5, and 7");
}

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 +87,18 @@ public bool Compute()
IsSuccessful = true;
try
{
switch (Version)
Guid guid = Version switch
{
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)
{
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;
GuidResult = guid;
}

Result = GuidResult.ToByteArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System.Collections.Generic;
using System.Globalization;
using System.Text;

using Community.PowerToys.Run.Plugin.ValueGenerator.Properties;

Expand All @@ -23,6 +22,7 @@ internal static class QueryHelper
private static readonly string GeneratorDescriptionUuidv3 = Resources.generator_description_uuidv3;
private static readonly string GeneratorDescriptionUuidv4 = Resources.generator_description_uuidv4;
private static readonly string GeneratorDescriptionUuidv5 = Resources.generator_description_uuidv5;
private static readonly string GeneratorDescriptionUuidv7 = Resources.generator_description_uuidv7;
private static readonly string GeneratorDescriptionHash = Resources.generator_description_hash;
private static readonly string GeneratorDescriptionBase64 = Resources.generator_description_base64;
private static readonly string GeneratorDescriptionBase64d = Resources.generator_description_base64d;
Expand Down Expand Up @@ -92,6 +92,12 @@ internal static string GetResultSubtitle(GeneratorData generatorData)
Example = $"uuidv5 ns:<DNS, URL, OID, {GetStringFormat(Or)} X500> <{GetStringFormat(GeneratorDescriptionYourInput)}>",
},
new()
{
Keyword = "uuidv7",
Description = GetStringFormat(GeneratorDescriptionUuidv7),
Example = $"uuidv7 {GetStringFormat(Or)} uuid7",
},
new()
{
Keyword = "md5",
Description = GetStringFormat(GeneratorDescriptionHash, "MD5"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public IComputeRequest ParseInput(Query query)

if (!int.TryParse(versionQuery, null, out version))
{
throw new FormatException("Could not determine requested GUID version. Supported versions are 1, 3, 4 and 5");
throw new FormatException("Could not determine requested GUID version. Supported versions are 1, 3, 4, 5, and 7");
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
<data name="generator_description_uuidv5" xml:space="preserve">
<value>Generate a version 5 (SHA1): Namespace and name based UUID</value>
</data>
<data name="generator_description_uuidv7" xml:space="preserve">
<value>Generate a version 7: Time-ordered randomly generated UUID</value>
</data>
<data name="generator_description_your_input" xml:space="preserve">
<value>your input</value>
<comment>Usage example: "md5 &lt;your input&gt;"</comment>
Expand Down
Loading