From 2c9ab5e45fd45d45909b9b348580c44bdfc4d36f Mon Sep 17 00:00:00 2001
From: Logan Stromberg <loganstromberg@gmail.com>
Date: Wed, 16 Nov 2022 14:53:17 -0800
Subject: [PATCH] Prevent raw Unicode control codes from showing on software
 keyboard applet. (#3845)

* Revert "Add support for releasing a semaphore to DmaClass (#2926)"

This reverts commit 521a07e6125d3a5d9781512639387a9be5f09107.

* Revert "Revert "Add support for releasing a semaphore to DmaClass (#2926)""

This reverts commit ec8a5fd05362f04cc77436ee3e45a9188777f75e.

* Strip non-visible control codes from strings before they are sent to the software keyboard to prevent ugly unicode blocks from being shown on the UI.

* remove debugging junk

* Initialize stringbuilder capacity at the start to prevent resizing (a tiny tiny microoptimization)

* Update remarks documentation. Remove unneeded imports.

* Removing a test that's actually just redundant

Co-authored-by: Logan Stromberg <lostromb@microsoft.com>
---
 Ryujinx.HLE/AssemblyInfo.cs                   |  3 +
 .../SoftwareKeyboardApplet.cs                 | 42 +++++++++--
 Ryujinx.Tests/HLE/SoftwareKeyboardTests.cs    | 71 +++++++++++++++++++
 3 files changed, 112 insertions(+), 4 deletions(-)
 create mode 100644 Ryujinx.HLE/AssemblyInfo.cs
 create mode 100644 Ryujinx.Tests/HLE/SoftwareKeyboardTests.cs

diff --git a/Ryujinx.HLE/AssemblyInfo.cs b/Ryujinx.HLE/AssemblyInfo.cs
new file mode 100644
index 000000000..9d7bad6be
--- /dev/null
+++ b/Ryujinx.HLE/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Ryujinx.Tests")]
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
index 3cfd192c7..e287318a6 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
@@ -204,12 +204,11 @@ namespace Ryujinx.HLE.HOS.Applets
             else
             {
                 // Call the configured GUI handler to get user's input.
-
                 var args = new SoftwareKeyboardUiArgs
                 {
-                    HeaderText = _keyboardForegroundConfig.HeaderText,
-                    SubtitleText = _keyboardForegroundConfig.SubtitleText,
-                    GuideText = _keyboardForegroundConfig.GuideText,
+                    HeaderText = StripUnicodeControlCodes(_keyboardForegroundConfig.HeaderText),
+                    SubtitleText = StripUnicodeControlCodes(_keyboardForegroundConfig.SubtitleText),
+                    GuideText = StripUnicodeControlCodes(_keyboardForegroundConfig.GuideText),
                     SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ?
                     _keyboardForegroundConfig.SubmitText : "OK"),
                     StringLengthMin = _keyboardForegroundConfig.StringLengthMin,
@@ -764,6 +763,41 @@ namespace Ryujinx.HLE.HOS.Applets
             }
         }
 
+        /// <summary>
+        /// Removes all Unicode control code characters from the input string.
+        /// This includes CR/LF, tabs, null characters, escape characters,
+        /// and special control codes which are used for formatting by the real keyboard applet.
+        /// </summary>
+        /// <remarks>
+        /// Some games send special control codes (such as 0x13 "Device Control 3") as part of the string.
+        /// Future implementations of the emulated keyboard applet will need to handle these as well.
+        /// </remarks>
+        /// <param name="input">The input string to sanitize (may be null).</param>
+        /// <returns>The sanitized string.</returns>
+        internal static string StripUnicodeControlCodes(string input)
+        {
+            if (input is null)
+            {
+                return null;
+            }
+            
+            if (input.Length == 0)
+            {
+                return string.Empty;
+            }
+
+            StringBuilder sb = new StringBuilder(capacity: input.Length);
+            foreach (char c in input)
+            {
+                if (!char.IsControl(c))
+                {
+                    sb.Append(c);
+                }
+            }
+
+            return sb.ToString();
+        }
+
         private static T ReadStruct<T>(byte[] data)
             where T : struct
         {
diff --git a/Ryujinx.Tests/HLE/SoftwareKeyboardTests.cs b/Ryujinx.Tests/HLE/SoftwareKeyboardTests.cs
new file mode 100644
index 000000000..d16039ad3
--- /dev/null
+++ b/Ryujinx.Tests/HLE/SoftwareKeyboardTests.cs
@@ -0,0 +1,71 @@
+using NUnit.Framework;
+using Ryujinx.HLE.HOS.Applets;
+using System.Text;
+
+namespace Ryujinx.Tests.HLE
+{
+    public class SoftwareKeyboardTests
+    {
+        [Test]
+        public void StripUnicodeControlCodes_NullInput()
+        {
+            Assert.IsNull(SoftwareKeyboardApplet.StripUnicodeControlCodes(null));
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_EmptyInput()
+        {
+            Assert.AreEqual(string.Empty, SoftwareKeyboardApplet.StripUnicodeControlCodes(string.Empty));
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_Passthrough()
+        {
+            string[] prompts = new string[]
+            {
+                "Please name him.",
+                "Name her, too.",
+                "Name your friend.",
+                "Name another friend.",
+                "Name your pet.",
+                "Favorite homemade food?",
+                "What’s your favorite thing?",
+                "Are you sure?",
+            };
+
+            foreach (string prompt in prompts)
+            {
+                Assert.AreEqual(prompt, SoftwareKeyboardApplet.StripUnicodeControlCodes(prompt));
+            }
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_StripsNewlines()
+        {
+            Assert.AreEqual("I am very tall", SoftwareKeyboardApplet.StripUnicodeControlCodes("I \r\nam \r\nvery \r\ntall"));
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_StripsDeviceControls()
+        {
+            // 0x13 is control code DC3 used by some games
+            string specialInput = Encoding.UTF8.GetString(new byte[] { 0x13, 0x53, 0x68, 0x69, 0x6E, 0x65, 0x13 });
+            Assert.AreEqual("Shine", SoftwareKeyboardApplet.StripUnicodeControlCodes(specialInput));
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_StripsToEmptyString()
+        {
+            string specialInput = Encoding.UTF8.GetString(new byte[] { 17, 18, 19, 20 }); // DC1 - DC4 special codes
+            Assert.AreEqual(string.Empty, SoftwareKeyboardApplet.StripUnicodeControlCodes(specialInput));
+        }
+
+        [Test]
+        public void StripUnicodeControlCodes_PreservesMultiCodePoints()
+        {
+            // Turtles are a good example of multi-codepoint Unicode chars
+            string specialInput = "♀ 🐢 🐢 ♂ ";
+            Assert.AreEqual(specialInput, SoftwareKeyboardApplet.StripUnicodeControlCodes(specialInput));
+        }
+    }
+}