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
This commit is contained in:
kalimag 2024-06-03 10:08:50 +02:00 committed by GitHub
parent ec8ba06dbe
commit 1b961f248d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 168 additions and 3 deletions

View File

@ -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);
}
/// <returns><see langword="true"/> if regular paste handling should be prevented.</returns>
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)
{ }
/// <summary>
/// Paste <paramref name="text"/> at selected position without exceeding the <see cref="TextBoxBase.MaxLength"/> limit.
/// The pasted string will be truncated if necessary.
/// </summary>
/// <remarks>
/// Does not raise <see cref="OnPaste"/>.
/// </remarks>
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; }
/// <summary>Prevents regular paste handling if set to <see langword="true"/>.</summary>
public bool Handled { get; set; }
public PasteEventArgs(bool containsText, string text)
{
ContainsText = containsText;
Text = text;
}
}
}
}

View File

@ -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))

View File

@ -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

View File

@ -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
/// </returns>
public static string OnlyHex(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsHex)).ToUpperInvariant();
/// <returns>
/// A copy of <paramref name="raw"/> in uppercase after removing <c>0x</c>/<c>$</c> prefixes and all whitespace, or
/// <see cref="string.Empty"/> if <paramref name="raw"/> contains other non-hex characters.
/// </returns>
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|\$)?(?<hex>[0-9A-Fa-f\s]+)\s*$");
#if NET7_0_OR_GREATER
public static ushort ParseU16FromHex(ReadOnlySpan<char> str)
=> ushort.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);

View File

@ -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());
}
}
}