From 1b961f248deadec9d34189c646e96f825386122f Mon Sep 17 00:00:00 2001 From: kalimag Date: Mon, 3 Jun 2024 10:08:50 +0200 Subject: [PATCH] Sanitize text pasted into hex text boxes (squashed PR #3684) * Sanitize text pasted into hex text boxes Trim `0x` and `$` prefixes and whitespace pasted into `HexTextBox` and `WatchValueBox`. Prevent pasting non-hex text. Add `ClipboardEventTextBox` control with `OnPaste` event * Fall back to trapping paste keyboard shortcuts on Linux * Adjust code style, seal `PasteEventArgs` * Use slightly more sophisticated shared method for sanitizing hex strings * Use moderately more sophisticated method for sanitizing hex strings * More `string.Empty` * Add some comments * Code style * Remove superfluous format check --- .../CustomControls/ClipboardEventTextBox.cs | 94 +++++++++++++++++++ .../CustomControls/HexTextBox.cs | 16 +++- .../tools/Watch/WatchValueBox.cs | 16 +++- .../Extensions/NumericStringExtensions.cs | 18 ++++ .../NumericStringExtensionTests.cs | 27 ++++++ 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 src/BizHawk.Client.EmuHawk/CustomControls/ClipboardEventTextBox.cs create mode 100644 src/BizHawk.Tests/Common/StringExtensions/NumericStringExtensionTests.cs diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/ClipboardEventTextBox.cs b/src/BizHawk.Client.EmuHawk/CustomControls/ClipboardEventTextBox.cs new file mode 100644 index 0000000000..ab4237ac89 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/CustomControls/ClipboardEventTextBox.cs @@ -0,0 +1,94 @@ +using System; +using System.Windows.Forms; +using BizHawk.Common; + +namespace BizHawk.Client.EmuHawk.CustomControls +{ + public class ClipboardEventTextBox : TextBox + { + protected override void WndProc(ref Message m) + { + // WM_PASTE is also sent when pasting through the OS context menu, but doesn't work on Mono + const int WM_PASTE = 0x302; + + if (m.Msg is WM_PASTE && !OSTailoredCode.IsUnixHost) + { + if (OnPasteInternal()) + { + return; + } + } + + base.WndProc(ref m); + } + + protected override bool ProcessCmdKey(ref Message m, Keys keyData) + { + if (!ReadOnly && OSTailoredCode.IsUnixHost && keyData is (Keys.Control | Keys.V) or (Keys.Shift | Keys.Insert)) + { + return OnPasteInternal(); + } + + return base.ProcessCmdKey(ref m, keyData); + } + + /// if regular paste handling should be prevented. + private bool OnPasteInternal() + { + bool containsText; + string text; + + try + { + containsText = Clipboard.ContainsText(); + text = containsText ? Clipboard.GetText() : string.Empty; + } + catch (Exception) + { + // Clipboard is busy? No idea if this ever happens in practice + return true; + } + + var args = new PasteEventArgs(containsText, text); + OnPaste(args); + return args.Handled; + } + + protected virtual void OnPaste(PasteEventArgs e) + { } + + /// + /// Paste at selected position without exceeding the limit. + /// The pasted string will be truncated if necessary. + /// + /// + /// Does not raise . + /// + public void PasteWithMaxLength(string text) + { + if (MaxLength > 0) + { + var availableLength = MaxLength - TextLength + SelectionLength; + if (text.Length > availableLength) + { + text = text.Substring(startIndex: 0, length: availableLength); + } + } + Paste(text); + } + + protected sealed class PasteEventArgs : EventArgs + { + public bool ContainsText { get; } + public string Text { get; } + /// Prevents regular paste handling if set to . + public bool Handled { get; set; } + + public PasteEventArgs(bool containsText, string text) + { + ContainsText = containsText; + Text = text; + } + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/HexTextBox.cs b/src/BizHawk.Client.EmuHawk/CustomControls/HexTextBox.cs index 8a8d13e521..7196a110b8 100644 --- a/src/BizHawk.Client.EmuHawk/CustomControls/HexTextBox.cs +++ b/src/BizHawk.Client.EmuHawk/CustomControls/HexTextBox.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Windows.Forms; - +using BizHawk.Client.EmuHawk.CustomControls; using BizHawk.Common.StringExtensions; using BizHawk.Common.NumberExtensions; @@ -15,7 +15,7 @@ namespace BizHawk.Client.EmuHawk void SetFromRawInt(int? rawInt); } - public class HexTextBox : TextBox, INumberBox + public class HexTextBox : ClipboardEventTextBox, INumberBox { private string _addressFormatStr = ""; private long? _maxSize; @@ -135,6 +135,18 @@ namespace BizHawk.Client.EmuHawk base.OnTextChanged(e); } + protected override void OnPaste(PasteEventArgs e) + { + if (e.ContainsText) + { + string text = e.Text.CleanHex(); + PasteWithMaxLength(text); + e.Handled = true; + } + + base.OnPaste(e); + } + public int? ToRawInt() { if (string.IsNullOrWhiteSpace(Text)) diff --git a/src/BizHawk.Client.EmuHawk/tools/Watch/WatchValueBox.cs b/src/BizHawk.Client.EmuHawk/tools/Watch/WatchValueBox.cs index 08875e8cd3..79263daff9 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Watch/WatchValueBox.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Watch/WatchValueBox.cs @@ -3,11 +3,13 @@ using System.Globalization; using System.Linq; using System.Windows.Forms; using BizHawk.Client.Common; +using BizHawk.Client.EmuHawk.CustomControls; using BizHawk.Common.NumberExtensions; +using BizHawk.Common.StringExtensions; namespace BizHawk.Client.EmuHawk { - public class WatchValueBox : TextBox, INumberBox + public class WatchValueBox : ClipboardEventTextBox, INumberBox { private WatchSize _size = WatchSize.Byte; private WatchDisplayType _type = WatchDisplayType.Hex; @@ -431,6 +433,18 @@ namespace BizHawk.Client.EmuHawk base.OnTextChanged(e); } + protected override void OnPaste(PasteEventArgs e) + { + if (Type is WatchDisplayType.Hex && e.ContainsText) + { + string text = e.Text.CleanHex(); + PasteWithMaxLength(text); + e.Handled = true; + } + + base.OnPaste(e); + } + public int? ToRawInt() { try diff --git a/src/BizHawk.Common/Extensions/NumericStringExtensions.cs b/src/BizHawk.Common/Extensions/NumericStringExtensions.cs index 755108c245..78fd2453c5 100644 --- a/src/BizHawk.Common/Extensions/NumericStringExtensions.cs +++ b/src/BizHawk.Common/Extensions/NumericStringExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; namespace BizHawk.Common.StringExtensions { @@ -33,6 +34,23 @@ namespace BizHawk.Common.StringExtensions /// public static string OnlyHex(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsHex)).ToUpperInvariant(); + /// + /// A copy of in uppercase after removing 0x/$ prefixes and all whitespace, or + /// if contains other non-hex characters. + /// + public static string CleanHex(this string? raw) + { + if (raw is not null && CleanHexRegex.Match(raw) is { Success: true} match) + { + return match.Groups["hex"].Value.OnlyHex(); + } + else + { + return string.Empty; + } + } + private static readonly Regex CleanHexRegex = new(@"^\s*(?:0x|\$)?(?[0-9A-Fa-f\s]+)\s*$"); + #if NET7_0_OR_GREATER public static ushort ParseU16FromHex(ReadOnlySpan str) => ushort.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); diff --git a/src/BizHawk.Tests/Common/StringExtensions/NumericStringExtensionTests.cs b/src/BizHawk.Tests/Common/StringExtensions/NumericStringExtensionTests.cs new file mode 100644 index 0000000000..f68622b42c --- /dev/null +++ b/src/BizHawk.Tests/Common/StringExtensions/NumericStringExtensionTests.cs @@ -0,0 +1,27 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using BizHawk.Common.StringExtensions; + +namespace BizHawk.Tests.Common.StringExtensions +{ + [TestClass] + public class NumericStringExtensionTests + { + [TestMethod] + public void TesCleanHex() + { + Assert.AreEqual("0123456789ABCDEFABCDEF", "0123456789ABCDEFabcdef".CleanHex()); + Assert.AreEqual("ABCDEF", "0xABCDEF".CleanHex()); + Assert.AreEqual("ABCDEF", "$ABCDEF".CleanHex()); + Assert.AreEqual("ABCDEF", " AB CD\nEF ".CleanHex()); + Assert.AreEqual("ABCDEF", " 0xABCDEF ".CleanHex()); + + Assert.AreEqual(string.Empty, (null as string).CleanHex()); + Assert.AreEqual(string.Empty, string.Empty.CleanHex()); + Assert.AreEqual(string.Empty, "0x$ABCDEF".CleanHex()); + Assert.AreEqual(string.Empty, "$0xABCDEF".CleanHex()); + Assert.AreEqual(string.Empty, "$$ABCDEF".CleanHex()); + Assert.AreEqual(string.Empty, "ABCDEF$".CleanHex()); + Assert.AreEqual(string.Empty, "A!B.C(DE)F".CleanHex()); + } + } +}