From 72bdfb073b3524cfa728265e846d16f3b52b15f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Sat, 28 Mar 2026 22:09:06 +0100 Subject: [PATCH] CmdPal: Fix exception when converting calc result to different bases (#46176) ## Summary of the Pull Request This PR fixes an exception that prevents showing result for big items: - Uses `BigInteger` and custom base converter for secondary results menu items. - Adds extra error handler to prevent exception when creating a secondary menu item from showing the main result to the user. - Adds some unit tests for the new base converter. ## PR Checklist - [x] Closes: #46167 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../BaseConverterTests.cs | 145 ++++++++++++++++++ .../QueryTests.cs | 21 +++ .../Helper/BaseConverter.cs | 49 ++++++ .../Helper/ResultHelper.cs | 89 ++++++----- 4 files changed, 264 insertions(+), 40 deletions(-) create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs new file mode 100644 index 0000000000..50757f19a2 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs @@ -0,0 +1,145 @@ +// 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.Globalization; +using System.Numerics; +using Microsoft.CmdPal.Ext.Calc.Helper; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Calc.UnitTests; + +[TestClass] +public class BaseConverterTests +{ + // Hex tests + [DataTestMethod] + [DataRow(0L, "0x0")] + [DataRow(1L, "0x1")] + [DataRow(16L, "0x10")] + [DataRow(255L, "0xFF")] + [DataRow(-1L, "-0x1")] + [DataRow(long.MaxValue, "0x7FFFFFFFFFFFFFFF")] + public void Convert_Hex_ReturnsExpected_WhenCalled(long input, string expected) + { + var result = BaseConverter.Convert(new BigInteger(input), 16); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Convert_Hex_HandlesLargeValues_WhenCalled() + { + var large = BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture); + var result = BaseConverter.Convert(large, 16); + Assert.AreEqual("0x56BC75E2D630FFFFF", result); + } + + // Binary tests + [DataTestMethod] + [DataRow(0L, "0b0")] + [DataRow(1L, "0b1")] + [DataRow(10L, "0b1010")] + [DataRow(255L, "0b11111111")] + [DataRow(-5L, "-0b101")] + public void Convert_Binary_ReturnsExpected_WhenCalled(long input, string expected) + { + var result = BaseConverter.Convert(new BigInteger(input), 2); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Convert_Binary_HandlesLargeValues_WhenCalled() + { + var large = BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture); + var result = BaseConverter.Convert(large, 2); + Assert.IsTrue(result.StartsWith("0b", StringComparison.Ordinal)); + Assert.IsTrue(result.Length > 60); + } + + // Octal tests + [DataTestMethod] + [DataRow(0L, "0o0")] + [DataRow(1L, "0o1")] + [DataRow(8L, "0o10")] + [DataRow(255L, "0o377")] + [DataRow(-1L, "-0o1")] + [DataRow(-255L, "-0o377")] + [DataRow(long.MaxValue, "0o777777777777777777777")] + public void Convert_Octal_ReturnsExpected_WhenCalled(long input, string expected) + { + var result = BaseConverter.Convert(new BigInteger(input), 8); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Convert_Octal_HandlesLargeValues_WhenCalled() + { + var large = (BigInteger)long.MaxValue + 1; + var result = BaseConverter.Convert(large, 8); + Assert.AreEqual("0o1000000000000000000000", result); + } + + [TestMethod] + public void Convert_Octal_HandlesLargeNegativeValues_WhenCalled() + { + var value = -BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture); + var result = BaseConverter.Convert(value, 8); + Assert.IsTrue(result.StartsWith("-0o", StringComparison.Ordinal)); + } + + [TestMethod] + public void Convert_Hex_DecimalMaxValue_WhenCalled() + { + var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + Assert.AreEqual("0xFFFFFFFFFFFFFFFFFFFFFFFF", BaseConverter.Convert(value, 16)); + } + + [TestMethod] + public void Convert_Hex_NegativeDecimalMaxValue_WhenCalled() + { + var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + Assert.AreEqual("-0xFFFFFFFFFFFFFFFFFFFFFFFF", BaseConverter.Convert(value, 16)); + } + + [TestMethod] + public void Convert_Binary_DecimalMaxValue_WhenCalled() + { + var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + var result = BaseConverter.Convert(value, 2); + Assert.AreEqual("0b" + new string('1', 96), result); + } + + [TestMethod] + public void Convert_Binary_NegativeDecimalMaxValue_WhenCalled() + { + var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + var result = BaseConverter.Convert(value, 2); + Assert.AreEqual("-0b" + new string('1', 96), result); + } + + [TestMethod] + public void Convert_Octal_DecimalMaxValue_WhenCalled() + { + var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + Assert.AreEqual("0o" + new string('7', 32), BaseConverter.Convert(value, 8)); + } + + [TestMethod] + public void Convert_Octal_NegativeDecimalMaxValue_WhenCalled() + { + var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture); + Assert.AreEqual("-0o" + new string('7', 32), BaseConverter.Convert(value, 8)); + } + + // Invalid base + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(17)] + [DataRow(-1)] + public void Convert_ThrowsArgumentOutOfRange_WhenBaseInvalid(int toBase) + { + Assert.ThrowsException(() => BaseConverter.Convert(BigInteger.One, toBase)); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs index fa8a441d43..2a31631d64 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.CmdPal.Ext.Calc.Helper; using Microsoft.CmdPal.Ext.Calc.Pages; using Microsoft.CmdPal.Ext.UnitTestBase; +using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.CmdPal.Ext.Calc.UnitTests; @@ -82,4 +83,24 @@ public class QueryTests : CommandPaletteUnitTestBase Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}"); } + + [DataTestMethod] + [DataRow("2^64", "18446744073709551616", "0x10000000000000000")] + [DataRow("0-(2^64)", "-18446744073709551616", "-0x10000000000000000")] + public void TopLevelPageQuery_ReturnsResult_WhenIntegerExceedsInt64Bounds(string input, string expectedTitle, string expectedHexContext) + { + var settings = new Settings(outputUseEnglishFormat: true); + var page = new CalculatorListPage(settings); + + page.UpdateSearchText(string.Empty, input); + var results = page.GetItems(); + var result = results.FirstOrDefault(); + + Assert.AreEqual(1, results.Length, "Large integer results should still produce the main result item."); + Assert.IsNotNull(result); + Assert.AreEqual(expectedTitle, result!.Title); + Assert.IsTrue( + result.MoreCommands.OfType().Any(item => item.Title == expectedHexContext), + "Large integer results should still include integer conversion context items."); + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs new file mode 100644 index 0000000000..7602086dde --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs @@ -0,0 +1,49 @@ +// 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.Numerics; +using System.Text; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class BaseConverter +{ + private const string Digits = "0123456789ABCDEF"; + + public static string Convert(BigInteger value, int toBase) + { + var prefix = toBase switch + { + 2 => "0b", + 8 => "0o", + 16 => "0x", + _ => string.Empty, + }; + + if (toBase is < 2 or > 16) + { + throw new ArgumentOutOfRangeException(nameof(toBase), "Base must be between 2 and 16."); + } + + if (value == BigInteger.Zero) + { + return prefix + "0"; + } + + var abs = BigInteger.Abs(value); + var sb = new StringBuilder(); + + while (abs > 0) + { + var digit = (int)(abs % toBase); + sb.Insert(0, Digits[digit]); + abs /= toBase; + } + + var sign = value < 0 ? "-" : string.Empty; + + return sign + prefix + sb; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs index 0147f73c07..edc18d8068 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Numerics; using ManagedCommon; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -66,57 +67,65 @@ public static class ResultHelper } var decimalResult = roundedResult?.ToString(outputCulture); + var decimalValue = (decimal)roundedResult; List context = []; - if (decimal.IsInteger((decimal)roundedResult)) + try { - context.Add(new Separator()); - - var i = decimal.ToInt64((decimal)roundedResult); - - // hexadecimal - try + if (decimal.IsInteger(decimalValue)) { - var hexResult = "0x" + i.ToString("X", outputCulture); - context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex }) + context.Add(new Separator()); + + var i = (BigInteger)decimalValue; + + // hexadecimal + try { - Title = hexResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to hex format", ex); - } - - // binary - try - { - var binaryResult = "0b" + i.ToString("B", outputCulture); - context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary }) + var hexResult = BaseConverter.Convert(i, 16); + context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex }) + { + Title = hexResult, + }); + } + catch (Exception ex) { - Title = binaryResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to binary format", ex); - } + Logger.LogError("Error converting to hex format", ex); + } - // octal - try - { - var octalResult = "0o" + Convert.ToString(i, 8); - context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal }) + // binary + try { - Title = octalResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to octal format", ex); + var binaryResult = BaseConverter.Convert(i, 2); + context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary }) + { + Title = binaryResult, + }); + } + catch (Exception ex) + { + Logger.LogError("Error converting to binary format", ex); + } + + // octal + try + { + var octalResult = BaseConverter.Convert(i, 8); + context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal }) + { + Title = octalResult, + }); + } + catch (Exception ex) + { + Logger.LogError("Error converting to octal format", ex); + } } } + catch (Exception ex) + { + Logger.LogError("Error creating integer context items", ex); + } return new ListItem(new CopyTextCommand(decimalResult)) {