Skip to content

Commit

Permalink
Sanitize text pasted into hex text boxes (squashed PR #3684)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kalimag authored Jun 3, 2024
1 parent ec8ba06 commit 1b961f2
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 3 deletions.
94 changes: 94 additions & 0 deletions src/BizHawk.Client.EmuHawk/CustomControls/ClipboardEventTextBox.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
16 changes: 14 additions & 2 deletions src/BizHawk.Client.EmuHawk/CustomControls/HexTextBox.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -15,7 +15,7 @@ public interface INumberBox
void SetFromRawInt(int? rawInt);
}

public class HexTextBox : TextBox, INumberBox
public class HexTextBox : ClipboardEventTextBox, INumberBox
{
private string _addressFormatStr = "";
private long? _maxSize;
Expand Down Expand Up @@ -135,6 +135,18 @@ protected override void OnTextChanged(EventArgs e)
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))
Expand Down
16 changes: 15 additions & 1 deletion src/BizHawk.Client.EmuHawk/tools/Watch/WatchValueBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
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;
Expand Down Expand Up @@ -431,6 +433,18 @@ protected override void OnTextChanged(EventArgs e)
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
Expand Down
18 changes: 18 additions & 0 deletions src/BizHawk.Common/Extensions/NumericStringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace BizHawk.Common.StringExtensions
{
Expand Down Expand Up @@ -33,6 +34,23 @@ public static class NumericStringExtensions
/// </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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}

0 comments on commit 1b961f2

Please sign in to comment.