mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 00:19:16 +02:00
Compare commits
2 Commits
powerscrip
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0557b362e6 | ||
|
|
cbb711ab73 |
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
@@ -25,7 +26,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
||||
return;
|
||||
}
|
||||
|
||||
Body = model.Body;
|
||||
Body = MarkdownTextHelper.SanitizeMarkdown(model.Body);
|
||||
UpdateProperty(nameof(Body));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
@@ -55,7 +56,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Body):
|
||||
Body = model.Body;
|
||||
Body = MarkdownTextHelper.SanitizeMarkdown(model.Body);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
@@ -34,7 +35,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
}
|
||||
|
||||
Title = model.Title ?? string.Empty;
|
||||
Body = model.Body ?? string.Empty;
|
||||
Body = MarkdownTextHelper.SanitizeMarkdown(model.Body);
|
||||
HeroImage = new(model.HeroImage);
|
||||
HeroImage.InitializeProperties();
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for sanitizing text before passing it to the WinUI MarkdownTextBlock
|
||||
/// control, which wraps RichTextBlock internally. Certain control characters can trigger a native
|
||||
/// crash (access violation) in RichTextBlock's text-selection code path when the user double-clicks
|
||||
/// to select a word.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See <see href="https://github.com/microsoft/microsoft-ui-xaml/issues/7299"/> for the upstream
|
||||
/// WinUI bug. This is a best-effort, client-side mitigation.
|
||||
/// </remarks>
|
||||
internal static partial class MarkdownTextHelper
|
||||
{
|
||||
// Matches characters that are known to destabilize WinUI's RichTextBlock:
|
||||
// - C0 control codes (U+0000–U+001F) except TAB (U+0009), LF (U+000A), CR (U+000D)
|
||||
// - DEL (U+007F)
|
||||
// - C1 control codes (U+0080–U+009F)
|
||||
[GeneratedRegex(@"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex ProblematicControlCharsRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Removes control characters from <paramref name="text"/> that are known to cause crashes in
|
||||
/// the WinUI <c>RichTextBlock</c> control's double-tap word-selection code path.
|
||||
/// Standard whitespace (TAB, LF, CR) is preserved because Markdown relies on it.
|
||||
/// </summary>
|
||||
/// <param name="text">The raw markdown string, possibly <see langword="null"/>.</param>
|
||||
/// <returns>The sanitized string, or <see cref="string.Empty"/> if <paramref name="text"/> is null.</returns>
|
||||
internal static string SanitizeMarkdown(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return text ?? string.Empty;
|
||||
}
|
||||
|
||||
return ProblematicControlCharsRegex().Replace(text, string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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 Microsoft.CmdPal.UI.ViewModels.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class MarkdownTextHelperTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_NullInput_ReturnsEmpty()
|
||||
{
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(null);
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_EmptyInput_ReturnsEmpty()
|
||||
{
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(string.Empty);
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_CleanMarkdown_ReturnsUnchanged()
|
||||
{
|
||||
const string input = "# Hello\n\nThis is **bold** and _italic_.\n\n- Item 1\n- Item 2\n";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual(input, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("\x00")] // NUL
|
||||
[DataRow("\x01")] // SOH
|
||||
[DataRow("\x07")] // BEL
|
||||
[DataRow("\x08")] // BS
|
||||
[DataRow("\x0B")] // VT (Vertical Tab)
|
||||
[DataRow("\x0C")] // FF (Form Feed)
|
||||
[DataRow("\x0E")] // SO
|
||||
[DataRow("\x1F")] // US
|
||||
[DataRow("\x7F")] // DEL
|
||||
[DataRow("\x80")] // C1 start
|
||||
[DataRow("\x9F")] // C1 end
|
||||
public void SanitizeMarkdown_SingleControlChar_IsRemoved(string controlChar)
|
||||
{
|
||||
var input = $"before{controlChar}after";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual("beforeafter", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_NulBytesWithinText_AreRemoved()
|
||||
{
|
||||
// NUL bytes in clipboard content (the main bug trigger from the issue)
|
||||
var input = "Hello\x00World\x00";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual("HelloWorld", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_StandardWhitespacePreserved()
|
||||
{
|
||||
// TAB (0x09), LF (0x0A), CR (0x0D) must be kept — Markdown relies on them
|
||||
const string input = "Column1\tColumn2\r\nLine2\nLine3";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual(input, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_MixedControlAndNormalChars_OnlyControlCharsRemoved()
|
||||
{
|
||||
// Simulates clipboard text with embedded NUL bytes interspersed with normal text
|
||||
var input = "This is a short \x00demo paragraph, meant \x00to include literal N\x00UL bytes.";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual("This is a short demo paragraph, meant to include literal NUL bytes.", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SanitizeMarkdown_UnicodeAndEmoji_NotAffected()
|
||||
{
|
||||
// Non-ASCII and emoji must pass through unchanged
|
||||
const string input = "Café ☕ \U0001F600 résumé";
|
||||
var result = MarkdownTextHelper.SanitizeMarkdown(input);
|
||||
Assert.AreEqual(input, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user