diff --git a/PowerToys.sln b/PowerToys.sln index c5d1f9176f69..a4dd741e190b 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -2294,6 +2294,14 @@ Global {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.Build.0 = Release|ARM64 {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.ActiveCfg = Release|x64 {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.Build.0 = Release|x64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|ARM64.Build.0 = Debug|ARM64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|x64.ActiveCfg = Debug|x64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|x64.Build.0 = Debug|x64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|ARM64.ActiveCfg = Release|ARM64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|ARM64.Build.0 = Release|ARM64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|x64.ActiveCfg = Release|x64 + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2534,6 +2542,7 @@ Global {4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} + {50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734} = {929C1324-22E8-4412-A9A8-80E85F3985A5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/registrypreview/RegistryPreview.FuzzTests/FuzzTests.cs b/src/modules/registrypreview/RegistryPreview.FuzzTests/FuzzTests.cs new file mode 100644 index 000000000000..3a681855bd06 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreview.FuzzTests/FuzzTests.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using System; +using System.Diagnostics; +using System.Globalization; +using System.Resources; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Win32; +using RegistryPreviewUILib; + +namespace RegistryPreview.FuzzTests +{ + public class FuzzTests + { + private const string REGISTRYHEADER4 = "regedit4"; + private const string REGISTRYHEADER5 = "windows registry editor version 5.00"; + private const string KEYIMAGE = "ms-appx:///Assets/RegistryPreview/folder32.png"; + private const string DELETEDKEYIMAGE = "ms-appx:///Assets/RegistryPreview/deleted-folder32.png"; + + // Case 1: Fuzz test for CheckKeyLineForBrackets + public static void FuzzCheckKeyLineForBrackets(ReadOnlySpan input) + { + string registryLine; + + // Simulate registry file content as filenameText + var filenameText = GenerateRegistryHeader(input); + + string[] registryLines = filenameText.Split("\r"); + + if (registryLines.Length <= 1) + { + return; + } + + // REG files have to start with one of two headers and it's case-insensitive + // The header in the registry file is either REGISTRYHEADER4 or REGISTRYHEADER5 + registryLine = registryLines[0]; + + // Check if the registry header is valid + if (!IsValidRegistryHeader(registryLine)) + { + return; + } + + int index = 1; + registryLine = registryLines[index]; // Extract content after the header + + ParseHelper.ProcessRegistryLine(registryLine); + if (registryLine.StartsWith("[-", StringComparison.InvariantCulture)) + { + // remove the - as we won't need it but it will get special treatment in the UI + registryLine = registryLine.Remove(1, 1); + + string imageName = DELETEDKEYIMAGE; + + // Fuzz test for the CheckKeyLineForBrackets method + ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName); + } + else if (registryLine.StartsWith('[')) + { + string imageName = KEYIMAGE; + + // Fuzz test for the CheckKeyLineForBrackets method + ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName); + } + else + { + return; + } + } + + // Case 2: Fuzz test for StripFirstAndLast + public static void FuzzStripFirstAndLast(ReadOnlySpan input) + { + string registryLine; + + var filenameText = GenerateRegistryHeader(input); + + filenameText = filenameText.Replace("\r\n", "\r"); + string[] registryLines = filenameText.Split("\r"); + + if (registryLines.Length <= 1) + { + return; + } + + // REG files have to start with one of two headers and it's case-insensitive + registryLine = registryLines[0]; + + if (!IsValidRegistryHeader(registryLine)) + { + return; + } + + int index = 1; + registryLine = registryLines[index]; + + ParseHelper.ProcessRegistryLine(registryLine); + + if (registryLine.StartsWith("[-", StringComparison.InvariantCulture)) + { + // remove the - as we won't need it but it will get special treatment in the UI + registryLine = registryLine.Remove(1, 1); + + string imageName = DELETEDKEYIMAGE; + ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName); + + // Fuzz test for the StripFirstAndLast method + registryLine = ParseHelper.StripFirstAndLast(registryLine); + } + else if (registryLine.StartsWith('[')) + { + string imageName = KEYIMAGE; + ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName); + + // Fuzz test for the StripFirstAndLast method + registryLine = ParseHelper.StripFirstAndLast(registryLine); + } + else if (registryLine.StartsWith('"') && registryLine.EndsWith("=-", StringComparison.InvariantCulture)) + { + registryLine = registryLine.Replace("=-", string.Empty); + + // remove the "'s without removing all of them + // Fuzz test for the StripFirstAndLast method + registryLine = ParseHelper.StripFirstAndLast(registryLine); + } + else if (registryLine.StartsWith('"')) + { + int equal = registryLine.IndexOf('='); + if ((equal < 0) || (equal > registryLine.Length - 1)) + { + // something is very wrong + return; + } + + // set the name and the value + string name = registryLine.Substring(0, equal); + + // trim the whitespace and quotes from the name + name = name.Trim(); + + // Fuzz test for the StripFirstAndLast method + name = ParseHelper.StripFirstAndLast(name); + + // Clean out any escaped characters in the value, only for the preview + name = ParseHelper.StripEscapedCharacters(name); + + // set the value + string value = registryLine.Substring(equal + 1); + + // trim the whitespace from the value + value = value.Trim(); + + // if the first character is a " then this is a string value, so find the last most " which will avoid comments + if (value.StartsWith('"')) + { + int last = value.LastIndexOf('"'); + if (last >= 0) + { + value = value.Substring(0, last + 1); + } + } + + if (value.StartsWith('"') && value.EndsWith('"')) + { + value = ParseHelper.StripFirstAndLast(value); + } + } + else + { + return; + } + } + + public static string GenerateRegistryHeader(ReadOnlySpan input) + { + string header = new Random().Next(2) == 0 ? REGISTRYHEADER4 : REGISTRYHEADER5; + + string inputText = System.Text.Encoding.UTF8.GetString(input); + string filenameText = header + "\r\n" + inputText; + + return filenameText.Replace("\r\n", "\r"); + } + + private static bool IsValidRegistryHeader(string line) + { + // Convert the line to lowercase once for comparison + var lineLower = line.ToLowerInvariant(); + + switch (line) + { + case REGISTRYHEADER4: + case REGISTRYHEADER5: + return true; + default: + return false; + } + } + } +} diff --git a/src/modules/registrypreview/RegistryPreview.FuzzTests/MSTestSettings.cs b/src/modules/registrypreview/RegistryPreview.FuzzTests/MSTestSettings.cs new file mode 100644 index 000000000000..5b05c0b86e3f --- /dev/null +++ b/src/modules/registrypreview/RegistryPreview.FuzzTests/MSTestSettings.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/src/modules/registrypreview/RegistryPreview.FuzzTests/OneFuzzConfig.json b/src/modules/registrypreview/RegistryPreview.FuzzTests/OneFuzzConfig.json new file mode 100644 index 000000000000..a074f41123f0 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreview.FuzzTests/OneFuzzConfig.json @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +{ + "configVersion": 3, + "entries": [ + { + "fuzzer": { + "$type": "libfuzzerDotNet", + "dll": "RegistryPreview.FuzzTests.dll", + "class": "RegistryPreview.FuzzTests.FuzzTests", + "method": "FuzzCheckKeyLineForBrackets", + "FuzzingTargetBinaries": [ + "PowerToys.RegistryPreview.dll" + ] + }, + "adoTemplate": { + // supply the values appropriate to your + // project, where bugs will be filed + "org": "microsoft", + "project": "OS", + "AssignedTo": "mengyuanchen@microsoft.com", + "AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys", + "IterationPath": "OS\\Future" + }, + "jobNotificationEmail": "mengyuanchen@microsoft.com", + "skip": false, + "rebootAfterSetup": false, + "oneFuzzJobs": [ + // at least one job is required + { + "projectName": "RegistryPreview", + "targetName": "RegistryPreview-dotnet-CheckKeyLineForBrackets-fuzzer" + } + ], + "jobDependencies": [ + // this should contain, at minimum, + // the DLL and PDB files + // you will need to add any other files required + // (globs are supported) + "RegistryPreview.FuzzTests.dll", + "RegistryPreview.FuzzTests.pdb", + "Microsoft.Windows.SDK.NET.dll", + "WinRT.Runtime.dll" + ], + "SdlWorkItemId": 49911822 + }, + { + "fuzzer": { + "$type": "libfuzzerDotNet", + "dll": "RegistryPreview.FuzzTests.dll", + "class": "RegistryPreview.FuzzTests.FuzzTests", + "method": "FuzzStripFirstAndLast", + "FuzzingTargetBinaries": [ + "PowerToys.RegistryPreview.dll" + ] + }, + "adoTemplate": { + // supply the values appropriate to your + // project, where bugs will be filed + "org": "microsoft", + "project": "OS", + "AssignedTo": "mengyuanchen@microsoft.com", + "AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys", + "IterationPath": "OS\\Future" + }, + "jobNotificationEmail": "mengyuanchen@microsoft.com", + "skip": false, + "rebootAfterSetup": false, + "oneFuzzJobs": [ + // at least one job is required + { + "projectName": "RegistryPreview", + "targetName": "RegistryPreview-dotnet-StripFirstAndLasts-fuzzer" + } + ], + "jobDependencies": [ + // this should contain, at minimum, + // the DLL and PDB files + // you will need to add any other files required + // (globs are supported) + "RegistryPreview.FuzzTests.dll", + "RegistryPreview.FuzzTests.pdb", + "Microsoft.Windows.SDK.NET.dll", + "WinRT.Runtime.dll" + ], + "SdlWorkItemId": 49911822 + } + ] +} \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreview.FuzzTests/RegistryPreview.FuzzTests.csproj b/src/modules/registrypreview/RegistryPreview.FuzzTests/RegistryPreview.FuzzTests.csproj new file mode 100644 index 000000000000..d59656463e3c --- /dev/null +++ b/src/modules/registrypreview/RegistryPreview.FuzzTests/RegistryPreview.FuzzTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0-windows10.0.19041.0 + x64;ARM64 + latest + enable + enable + + + + ..\..\..\..\$(Platform)\$(Configuration)\tests\RegistryPreview.FuzzTests\ + + + + + + + + + PreserveNewest + + + + + + + + + + + + diff --git a/src/modules/registrypreview/RegistryPreviewUILib/ParseHelper.cs b/src/modules/registrypreview/RegistryPreviewUILib/ParseHelper.cs new file mode 100644 index 000000000000..761ef338fd59 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUILib/ParseHelper.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RegistryPreviewUILib +{ + internal static class ParseHelper + { + private const string ERRORIMAGE = "ms-appx:///Assets/RegistryPreview/error32.png"; + + /// + /// Checks a Key line for the closing bracket and treat it as an error if it cannot be found + /// + internal static void CheckKeyLineForBrackets(ref string registryLine, ref string imageName) + { + // following the current behavior of the registry editor, find the last ] and treat everything else as ignorable + int lastBracket = registryLine.LastIndexOf(']'); + if (lastBracket == -1) + { + // since we don't have a last bracket yet, add an extra space and continue processing + registryLine += " "; + imageName = ERRORIMAGE; + } + else + { + // having found the last ] and there is text after it, drop the rest of the string on the floor + if (lastBracket < registryLine.Length - 1) + { + registryLine = registryLine.Substring(0, lastBracket + 1); + } + + if (CheckForKnownGoodBranches(registryLine) == false) + { + imageName = ERRORIMAGE; + } + } + } + + /// + /// Make sure the root of a full path start with one of the five "hard coded" roots. Throw an error for the branch if it doesn't. + /// + private static bool CheckForKnownGoodBranches(string key) + { + if ((key.StartsWith("[HKEY_CLASSES_ROOT]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKEY_CURRENT_USER]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKEY_USERS]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKEY_LOCAL_MACHINE]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKEY_CURRENT_CONFIG]", StringComparison.InvariantCultureIgnoreCase) == false) + && + (key.StartsWith(@"[HKEY_CLASSES_ROOT\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKEY_CURRENT_USER\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKEY_USERS\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKEY_LOCAL_MACHINE\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKEY_CURRENT_CONFIG\", StringComparison.InvariantCultureIgnoreCase) == false) + && + (key.StartsWith("[HKCR]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKCU]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKU]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKLM]", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith("[HKCC]", StringComparison.InvariantCultureIgnoreCase) == false) + && + (key.StartsWith(@"[HKCR\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKCU\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKU\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKLM\", StringComparison.InvariantCultureIgnoreCase) == false && + key.StartsWith(@"[HKCC\", StringComparison.InvariantCultureIgnoreCase) == false)) + { + return false; + } + + return true; + } + + /// + /// Rip the first and last character off a string, + /// checking that the string is at least 2 characters long to avoid errors + /// + internal static string StripFirstAndLast(string line) + { + if (line.Length > 1) + { + line = line.Remove(line.Length - 1, 1); + line = line.Remove(0, 1); + } + + return line; + } + + /// + /// Replace any escaped characters in the REG file with their counterparts, for the UX + /// + internal static string StripEscapedCharacters(string value) + { + value = value.Replace("\\\\", "\\"); // Replace \\ with \ in the UI + value = value.Replace("\\\"", "\""); // Replace \" with " in the UI + return value; + } + + // special case for when the registryLine begins with a @ - make some tweaks and + // let the regular processing handle the rest. + internal static string ProcessRegistryLine(string registryLine) + { + if (registryLine.StartsWith("@=-", StringComparison.InvariantCulture)) + { + // REG file has a callout to delete the @ Value which won't work *but* the Registry Editor will + // clear the value of the @ Value instead, so it's still a valid line. + registryLine = registryLine.Replace("@=-", "\"(Default)\"=\"\""); + } + else if (registryLine.StartsWith("@=", StringComparison.InvariantCulture)) + { + // This is the Value called "(Default)" so we tweak the line for the UX + registryLine = registryLine.Replace("@=", "\"(Default)\"="); + } + + return registryLine; + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs index b8fc6e4e1d9a..819fee949a84 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs @@ -967,12 +967,7 @@ private void SaveFile() /// private string StripFirstAndLast(string line) { - if (line.Length > 1) - { - line = line.Remove(line.Length - 1, 1); - line = line.Remove(0, 1); - } - + line = ParseHelper.StripFirstAndLast(line); return line; } @@ -1040,27 +1035,7 @@ private void SetValueToolTip(RegistryValue registryValue) /// private void CheckKeyLineForBrackets(ref string registryLine, ref string imageName) { - // following the current behavior of the registry editor, find the last ] and treat everything else as ignorable - int lastBracket = registryLine.LastIndexOf(']'); - if (lastBracket == -1) - { - // since we don't have a last bracket yet, add an extra space and continue processing - registryLine += " "; - imageName = ERRORIMAGE; - } - else - { - // having found the last ] and there is text after it, drop the rest of the string on the floor - if (lastBracket < registryLine.Length - 1) - { - registryLine = registryLine.Substring(0, lastBracket + 1); - } - - if (CheckForKnownGoodBranches(registryLine) == false) - { - imageName = ERRORIMAGE; - } - } + ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName); } /// @@ -1079,41 +1054,6 @@ private string ScanAndRemoveComments(string value) return value.TrimEnd(); } - /// - /// Make sure the root of a full path start with one of the five "hard coded" roots. Throw an error for the branch if it doesn't. - /// - private bool CheckForKnownGoodBranches(string key) - { - if ((key.StartsWith("[HKEY_CLASSES_ROOT]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKEY_CURRENT_USER]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKEY_USERS]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKEY_LOCAL_MACHINE]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKEY_CURRENT_CONFIG]", StringComparison.InvariantCultureIgnoreCase) == false) - && - (key.StartsWith(@"[HKEY_CLASSES_ROOT\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKEY_CURRENT_USER\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKEY_USERS\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKEY_LOCAL_MACHINE\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKEY_CURRENT_CONFIG\", StringComparison.InvariantCultureIgnoreCase) == false) - && - (key.StartsWith("[HKCR]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKCU]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKU]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKLM]", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith("[HKCC]", StringComparison.InvariantCultureIgnoreCase) == false) - && - (key.StartsWith(@"[HKCR\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKCU\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKU\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKLM\", StringComparison.InvariantCultureIgnoreCase) == false && - key.StartsWith(@"[HKCC\", StringComparison.InvariantCultureIgnoreCase) == false)) - { - return false; - } - - return true; - } - /// /// Turns the Open Key button in the command bar on/off, depending on if a key is selected ///