diff --git a/doc/devdocs/modules/launcher/plugins/community.valuegenerator.md b/doc/devdocs/modules/launcher/plugins/community.valuegenerator.md index 4a580f090d..9b94ae78f2 100644 --- a/doc/devdocs/modules/launcher/plugins/community.valuegenerator.md +++ b/doc/devdocs/modules/launcher/plugins/community.valuegenerator.md @@ -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) @@ -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 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 7e3bffb7e5..c7f976c134 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; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests @@ -56,6 +57,39 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests 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")] 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 970cedfc1f..65b630adcd 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 @@ -12,7 +12,7 @@ using Wox.Plugin; namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests { [TestClass] - public class InputParserTests + public partial class InputParserTests { [DataTestMethod] [DataRow("md5 abc", typeof(Hashing.HashRequest))] @@ -27,6 +27,8 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests [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)] @@ -90,25 +92,22 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests 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(); } } 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 e83649e327..8039877665 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 @@ -52,6 +52,11 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID 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(); 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 dc4392a744..718b4b709a 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,6 @@ using System; using System.Security.Cryptography; - using Wox.Plugin.Logger; namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID @@ -19,34 +18,15 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID 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; } @@ -60,20 +40,19 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID { 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; } @@ -108,20 +87,18 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.GUID 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(); diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Helper/QueryHelper.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Helper/QueryHelper.cs index d984ebe006..6fb73f5268 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Helper/QueryHelper.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Helper/QueryHelper.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; -using System.Text; using Community.PowerToys.Run.Plugin.ValueGenerator.Properties; @@ -23,6 +22,7 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.Helper 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; @@ -92,6 +92,12 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.Helper Example = $"uuidv5 ns: <{GetStringFormat(GeneratorDescriptionYourInput)}>", }, new() + { + Keyword = "uuidv7", + Description = GetStringFormat(GeneratorDescriptionUuidv7), + Example = $"uuidv7 {GetStringFormat(Or)} uuid7", + }, + new() { Keyword = "md5", Description = GetStringFormat(GeneratorDescriptionHash, "MD5"), diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/InputParser.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/InputParser.cs index cda6020ea6..e8bf4a577a 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/InputParser.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/InputParser.cs @@ -94,7 +94,7 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator 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"); } } diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.Designer.cs index 90c3adeff5..86c46aa615 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.Designer.cs @@ -213,6 +213,15 @@ namespace Community.PowerToys.Run.Plugin.ValueGenerator.Properties { } } + /// + /// Looks up a localized string similar to Generate a version 7: Time-ordered randomly generated UUID. + /// + public static string generator_description_uuidv7 { + get { + return ResourceManager.GetString("generator_description_uuidv7", resourceCulture); + } + } + /// /// Looks up a localized string similar to your input. /// diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.resx b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.resx index 1daf99ab71..a3f8a9aa8f 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.resx +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.ValueGenerator/Properties/Resources.resx @@ -168,6 +168,9 @@ Generate a version 5 (SHA1): Namespace and name based UUID + + Generate a version 7: Time-ordered randomly generated UUID + your input Usage example: "md5 <your input>"