From 48939df9a5ef1ff20f4f9717d1341b51a50dff14 Mon Sep 17 00:00:00 2001
From: Ekdohibs <nathanael.courant@laposte.net>
Date: Mon, 4 Apr 2016 18:31:00 +0200
Subject: [PATCH] Escape more strings: formspecs, item descriptions,
 infotexts...

Also, change the escape character to the more standard \x1b
Thus, it can be used in the future for translation or colored text,
for example.
---
 src/chat.cpp                    |  4 +--
 src/game.cpp                    |  8 +++--
 src/guiFormSpecMenu.cpp         | 54 ++++++++++++++-------------------
 src/guiFormSpecMenu.h           | 25 ++++++++++-----
 src/hud.cpp                     |  7 +++--
 src/unittest/test_utilities.cpp | 19 ++++++++++++
 src/util/string.cpp             | 27 -----------------
 src/util/string.h               | 44 +++++++++++++++++++++------
 8 files changed, 105 insertions(+), 83 deletions(-)

diff --git a/src/chat.cpp b/src/chat.cpp
index 7a5196ed5..ab945444f 100644
--- a/src/chat.cpp
+++ b/src/chat.cpp
@@ -679,8 +679,8 @@ ChatBackend::~ChatBackend()
 
 void ChatBackend::addMessage(std::wstring name, std::wstring text)
 {
-	name = removeChatEscapes(name);
-	text = removeChatEscapes(text);
+	name = unescape_enriched(name);
+	text = unescape_enriched(text);
 
 	// Note: A message may consist of multiple lines, for example the MOTD.
 	WStrfnd fnd(text);
diff --git a/src/game.cpp b/src/game.cpp
index d513517b7..23f261cfd 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -3731,7 +3731,7 @@ void Game::handlePointingAtNode(GameRunData *runData,
 	NodeMetadata *meta = map.getNodeMetadata(nodepos);
 
 	if (meta) {
-		infotext = utf8_to_wide(meta->getString("infotext"));
+		infotext = unescape_enriched(utf8_to_wide(meta->getString("infotext")));
 	} else {
 		MapNode n = map.getNodeNoEx(nodepos);
 
@@ -3807,13 +3807,15 @@ void Game::handlePointingAtObject(GameRunData *runData,
 		const v3f &player_position,
 		bool show_debug)
 {
-	infotext = utf8_to_wide(runData->selected_object->infoText());
+	infotext = unescape_enriched(
+		utf8_to_wide(runData->selected_object->infoText()));
 
 	if (show_debug) {
 		if (infotext != L"") {
 			infotext += L"\n";
 		}
-		infotext += utf8_to_wide(runData->selected_object->debugInfoText());
+		infotext += unescape_enriched(utf8_to_wide(
+			runData->selected_object->debugInfoText()));
 	}
 
 	if (input->getLeftState()) {
diff --git a/src/guiFormSpecMenu.cpp b/src/guiFormSpecMenu.cpp
index 1a6ee91cd..23cff3eb7 100644
--- a/src/guiFormSpecMenu.cpp
+++ b/src/guiFormSpecMenu.cpp
@@ -609,8 +609,6 @@ void GUIFormSpecMenu::parseButton(parserData* data,std::string element,
 		if(!data->explicit_size)
 			warningstream<<"invalid use of button without a size[] element"<<std::endl;
 
-		label = unescape_string(label);
-
 		std::wstring wlabel = utf8_to_wide(label);
 
 		FieldSpec spec(
@@ -733,7 +731,6 @@ void GUIFormSpecMenu::parseTable(parserData* data,std::string element)
 		geom.X = stof(v_geom[0]) * (float)spacing.X;
 		geom.Y = stof(v_geom[1]) * (float)spacing.Y;
 
-
 		core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
 		FieldSpec spec(
@@ -746,7 +743,7 @@ void GUIFormSpecMenu::parseTable(parserData* data,std::string element)
 		spec.ftype = f_Table;
 
 		for (unsigned int i = 0; i < items.size(); ++i) {
-			items[i] = unescape_string(items[i]);
+			items[i] = unescape_string(unescape_enriched(items[i]));
 		}
 
 		//now really show table
@@ -818,7 +815,7 @@ void GUIFormSpecMenu::parseTextList(parserData* data,std::string element)
 		spec.ftype = f_Table;
 
 		for (unsigned int i = 0; i < items.size(); ++i) {
-			items[i] = unescape_string(items[i]);
+			items[i] = unescape_string(unescape_enriched(items[i]));
 		}
 
 		//now really show list
@@ -889,7 +886,8 @@ void GUIFormSpecMenu::parseDropDown(parserData* data,std::string element)
 		}
 
 		for (unsigned int i=0; i < items.size(); i++) {
-			e->addItem(utf8_to_wide(items[i]).c_str());
+			e->addItem(unescape_string(unescape_enriched(
+				utf8_to_wide(items[i]))).c_str());
 		}
 
 		if (str_initial_selection != "")
@@ -930,8 +928,6 @@ void GUIFormSpecMenu::parsePwdField(parserData* data,std::string element)
 
 		core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
 
-		label = unescape_string(label);
-
 		std::wstring wlabel = utf8_to_wide(label);
 
 		FieldSpec spec(
@@ -995,8 +991,6 @@ void GUIFormSpecMenu::parseSimpleField(parserData* data,
 	if(m_form_src)
 		default_val = m_form_src->resolveText(default_val);
 
-	default_val = unescape_string(default_val);
-	label = unescape_string(label);
 
 	std::wstring wlabel = utf8_to_wide(label);
 
@@ -1094,9 +1088,6 @@ void GUIFormSpecMenu::parseTextArea(parserData* data,
 		default_val = m_form_src->resolveText(default_val);
 
 
-	default_val = unescape_string(default_val);
-	label = unescape_string(label);
-
 	std::wstring wlabel = utf8_to_wide(label);
 
 	FieldSpec spec(
@@ -1197,7 +1188,6 @@ void GUIFormSpecMenu::parseLabel(parserData* data,std::string element)
 		if(!data->explicit_size)
 			warningstream<<"invalid use of label without a size[] element"<<std::endl;
 
-		text = unescape_string(text);
 		std::vector<std::string> lines = split(text, '\n');
 
 		for (unsigned int i = 0; i != lines.size(); i++) {
@@ -1243,7 +1233,8 @@ void GUIFormSpecMenu::parseVertLabel(parserData* data,std::string element)
 		((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION)))
 	{
 		std::vector<std::string> v_pos = split(parts[0],',');
-		std::wstring text = utf8_to_wide(unescape_string(parts[1]));
+		std::wstring text = unescape_string(
+			unescape_enriched(utf8_to_wide(parts[1])));
 
 		MY_CHECKPOS("vertlabel",1);
 
@@ -1330,7 +1321,6 @@ void GUIFormSpecMenu::parseImageButton(parserData* data,std::string element,
 
 		image_name = unescape_string(image_name);
 		pressed_image_name = unescape_string(pressed_image_name);
-		label = unescape_string(label);
 
 		std::wstring wlabel = utf8_to_wide(label);
 
@@ -1430,7 +1420,8 @@ void GUIFormSpecMenu::parseTabHeader(parserData* data,std::string element)
 		e->setNotClipped(true);
 
 		for (unsigned int i = 0; i < buttons.size(); i++) {
-			e->addTab(utf8_to_wide(buttons[i]).c_str(), -1);
+			e->addTab(unescape_string(unescape_enriched(
+				utf8_to_wide(buttons[i]))).c_str(), -1);
 		}
 
 		if ((tab_index >= 0) &&
@@ -1489,7 +1480,6 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data,std::string element)
 						m_default_tooltip_bgcolor,
 						m_default_tooltip_color);
 
-		label = unescape_string(label);
 		FieldSpec spec(
 			name,
 			utf8_to_wide(label),
@@ -1604,14 +1594,14 @@ void GUIFormSpecMenu::parseTooltip(parserData* data, std::string element)
 	std::vector<std::string> parts = split(element,';');
 	if (parts.size() == 2) {
 		std::string name = parts[0];
-		m_tooltips[name] = TooltipSpec(unescape_string(parts[1]),
+		m_tooltips[name] = TooltipSpec(parts[1],
 			m_default_tooltip_bgcolor, m_default_tooltip_color);
 		return;
 	} else if (parts.size() == 4) {
 		std::string name = parts[0];
 		video::SColor tmp_color1, tmp_color2;
 		if ( parseColorString(parts[2], tmp_color1, false) && parseColorString(parts[3], tmp_color2, false) ) {
-			m_tooltips[name] = TooltipSpec(unescape_string(parts[1]),
+			m_tooltips[name] = TooltipSpec(parts[1],
 				tmp_color1, tmp_color2);
 			return;
 		}
@@ -2242,16 +2232,18 @@ void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase,
 			}
 
 			// Draw tooltip
-			std::string tooltip_text = "";
-			if (hovering && !m_selected_item)
-				tooltip_text = item.getDefinition(m_gamedef->idef()).description;
-			if (tooltip_text != "") {
-				std::vector<std::string> tt_rows = str_split(tooltip_text, '\n');
+			std::wstring tooltip_text = L"";
+			if (hovering && !m_selected_item) {
+				tooltip_text = utf8_to_wide(item.getDefinition(m_gamedef->idef()).description);
+				tooltip_text = unescape_enriched(tooltip_text);
+			}
+			if (tooltip_text != L"") {
+				std::vector<std::wstring> tt_rows = str_split(tooltip_text, L'\n');
 				m_tooltip_element->setBackgroundColor(m_default_tooltip_bgcolor);
 				m_tooltip_element->setOverrideColor(m_default_tooltip_color);
 				m_tooltip_element->setVisible(true);
 				this->bringToFront(m_tooltip_element);
-				m_tooltip_element->setText(utf8_to_wide(tooltip_text).c_str());
+				m_tooltip_element->setText(tooltip_text.c_str());
 				s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height;
 				s32 tooltip_height = m_tooltip_element->getTextHeight() + 5;
 				v2u32 screenSize = driver->getScreenSize();
@@ -2504,7 +2496,7 @@ void GUIFormSpecMenu::drawMenu()
 		u32 delta = 0;
 		if (id == -1) {
 			m_old_tooltip_id = id;
-			m_old_tooltip = "";
+			m_old_tooltip = L"";
 		} else {
 			if (id == m_old_tooltip_id) {
 				delta = porting::getDeltaMs(m_hovered_time, getTimeMs());
@@ -2517,11 +2509,11 @@ void GUIFormSpecMenu::drawMenu()
 		if (id != -1 && delta >= m_tooltip_show_delay) {
 			for(std::vector<FieldSpec>::iterator iter =  m_fields.begin();
 					iter != m_fields.end(); ++iter) {
-				if ( (iter->fid == id) && (m_tooltips[iter->fname].tooltip != "") ){
+				if (iter->fid == id && m_tooltips[iter->fname].tooltip != L"") {
 					if (m_old_tooltip != m_tooltips[iter->fname].tooltip) {
 						m_old_tooltip = m_tooltips[iter->fname].tooltip;
-						m_tooltip_element->setText(utf8_to_wide(m_tooltips[iter->fname].tooltip).c_str());
-						std::vector<std::string> tt_rows = str_split(m_tooltips[iter->fname].tooltip, '\n');
+						m_tooltip_element->setText(m_tooltips[iter->fname].tooltip.c_str());
+						std::vector<std::wstring> tt_rows = str_split(m_tooltips[iter->fname].tooltip, L'\n');
 						s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height;
 						s32 tooltip_height = m_tooltip_element->getTextHeight() * tt_rows.size() + 5;
 						int tooltip_offset_x = m_btn_height;
@@ -2875,7 +2867,7 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
 				core::position2d<s32>(x, y));
 		if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
 			m_old_tooltip_id = -1;
-			m_old_tooltip = "";
+			m_old_tooltip = L"";
 		}
 		if (!isChild(hovered,this)) {
 			if (DoubleClickDetection(event)) {
diff --git a/src/guiFormSpecMenu.h b/src/guiFormSpecMenu.h
index 005b91369..8774d306f 100644
--- a/src/guiFormSpecMenu.h
+++ b/src/guiFormSpecMenu.h
@@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "modalMenu.h"
 #include "guiTable.h"
 #include "network/networkprotocol.h"
+#include "util/string.h"
 
 class IGameDef;
 class InventoryManager;
@@ -191,18 +192,26 @@ class GUIFormSpecMenu : public GUIModalMenu
 		bool scale;
 	};
 
+	/* The responsibility of unescaping the strings has been shifted
+	 * from the formspec parsing methods to the draw methods.
+	 * There still are a few exceptions:
+	 *  - Vertical label, because it modifies the string by inserting
+	 *    '\n' between each character,
+	 *  - Tab header, because it gives the string immediately to
+	 *    Irrlicht and we can't unescape it later.
+	 */
 	struct FieldSpec
 	{
 		FieldSpec()
 		{
 		}
 		FieldSpec(const std::string &name, const std::wstring &label,
-				const std::wstring &fdeflt, int id) :
+				const std::wstring &default_text, int id) :
 			fname(name),
-			flabel(label),
-			fdefault(fdeflt),
 			fid(id)
 		{
+			flabel = unescape_string(unescape_enriched(label));
+			fdefault = unescape_string(unescape_enriched(default_text));
 			send = false;
 			ftype = f_Unknown;
 			is_exit = false;
@@ -235,12 +244,12 @@ class GUIFormSpecMenu : public GUIModalMenu
 		}
 		TooltipSpec(std::string a_tooltip, irr::video::SColor a_bgcolor,
 				irr::video::SColor a_color):
-			tooltip(a_tooltip),
 			bgcolor(a_bgcolor),
 			color(a_color)
 		{
+			tooltip = unescape_string(unescape_enriched(utf8_to_wide(a_tooltip)));
 		}
-		std::string tooltip;
+		std::wstring tooltip;
 		irr::video::SColor bgcolor;
 		irr::video::SColor color;
 	};
@@ -252,18 +261,18 @@ class GUIFormSpecMenu : public GUIModalMenu
 		}
 		StaticTextSpec(const std::wstring &a_text,
 				const core::rect<s32> &a_rect):
-			text(a_text),
 			rect(a_rect),
 			parent_button(NULL)
 		{
+			text = unescape_string(unescape_enriched(a_text));
 		}
 		StaticTextSpec(const std::wstring &a_text,
 				const core::rect<s32> &a_rect,
 				gui::IGUIButton *a_parent_button):
-			text(a_text),
 			rect(a_rect),
 			parent_button(a_parent_button)
 		{
+			text = unescape_string(unescape_enriched(a_text));
 		}
 		std::wstring text;
 		core::rect<s32> rect;
@@ -406,7 +415,7 @@ class GUIFormSpecMenu : public GUIModalMenu
 	u32 m_tooltip_show_delay;
 	s32 m_hovered_time;
 	s32 m_old_tooltip_id;
-	std::string m_old_tooltip;
+	std::wstring m_old_tooltip;
 
 	bool m_rmouse_auto_place;
 
diff --git a/src/hud.cpp b/src/hud.cpp
index 502865caa..19feaef7b 100644
--- a/src/hud.cpp
+++ b/src/hud.cpp
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "hud.h"
 #include "settings.h"
 #include "util/numeric.h"
+#include "util/string.h"
 #include "log.h"
 #include "gamedef.h"
 #include "itemdef.h"
@@ -319,7 +320,7 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
 										 (e->number >> 8)  & 0xFF,
 										 (e->number >> 0)  & 0xFF);
 				core::rect<s32> size(0, 0, e->scale.X, text_height * e->scale.Y);
-				std::wstring text = utf8_to_wide(e->text);
+				std::wstring text = unescape_enriched(utf8_to_wide(e->text));
 				core::dimension2d<u32> textsize = font->getDimension(text.c_str());
 				v2s32 offset((e->align.X - 1.0) * (textsize.Width / 2),
 				             (e->align.Y - 1.0) * (textsize.Height / 2));
@@ -355,11 +356,11 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
 										 (e->number >> 8)  & 0xFF,
 										 (e->number >> 0)  & 0xFF);
 				core::rect<s32> size(0, 0, 200, 2 * text_height);
-				std::wstring text = utf8_to_wide(e->name);
+				std::wstring text = unescape_enriched(utf8_to_wide(e->name));
 				font->draw(text.c_str(), size + pos, color);
 				std::ostringstream os;
 				os << distance << e->text;
-				text = utf8_to_wide(os.str());
+				text = unescape_enriched(utf8_to_wide(os.str()));
 				pos.Y += text_height;
 				font->draw(text.c_str(), size + pos, color);
 				break; }
diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp
index 1785997de..d73975b9f 100644
--- a/src/unittest/test_utilities.cpp
+++ b/src/unittest/test_utilities.cpp
@@ -45,6 +45,7 @@ class TestUtilities : public TestBase {
 	void testStringAllowed();
 	void testAsciiPrintableHelper();
 	void testUTF8();
+	void testRemoveEscapes();
 	void testWrapRows();
 	void testIsNumber();
 	void testIsPowerOfTwo();
@@ -71,6 +72,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
 	TEST(testStringAllowed);
 	TEST(testAsciiPrintableHelper);
 	TEST(testUTF8);
+	TEST(testRemoveEscapes);
 	TEST(testWrapRows);
 	TEST(testIsNumber);
 	TEST(testIsPowerOfTwo);
@@ -253,6 +255,23 @@ void TestUtilities::testUTF8()
 		== "the shovel dug a crumbly node!");
 }
 
+void TestUtilities::testRemoveEscapes()
+{
+	UASSERT(unescape_enriched<wchar_t>(
+		L"abc\x1bXdef") == L"abcdef");
+	UASSERT(unescape_enriched<wchar_t>(
+		L"abc\x1b(escaped)def") == L"abcdef");
+	UASSERT(unescape_enriched<wchar_t>(
+		L"abc\x1b((escaped with parenthesis\\))def") == L"abcdef");
+	UASSERT(unescape_enriched<wchar_t>(
+		L"abc\x1b(incomplete") == L"abc");
+	UASSERT(unescape_enriched<wchar_t>(
+		L"escape at the end\x1b") == L"escape at the end");
+	// Nested escapes not supported
+	UASSERT(unescape_enriched<wchar_t>(
+		L"abc\x1b(outer \x1b(inner escape)escape)def") == L"abcescape)def");
+}
+
 void TestUtilities::testWrapRows()
 {
 	UASSERT(wrap_rows("12345678",4) == "1234\n5678");
diff --git a/src/util/string.cpp b/src/util/string.cpp
index c8f528a77..2c4143c76 100644
--- a/src/util/string.cpp
+++ b/src/util/string.cpp
@@ -729,33 +729,6 @@ static bool parseNamedColorString(const std::string &value, video::SColor &color
 	return true;
 }
 
-std::wstring removeChatEscapes(const std::wstring &s) {
-	std::wstring output;
-	size_t i = 0;
-	while (i < s.length()) {
-		if (s[i] == L'\v') {
-			++i;
-			if (i == s.length()) continue;
-			if (s[i] == L'(') {
-				++i;
-				while (i < s.length() && s[i] != L')') {
-					if (s[i] == L'\\') {
-						++i;
-					}
-					++i;
-				}
-				++i;
-			} else {
-				++i;
-			}
-			continue;
-		}
-		output += s[i];
-		++i;
-	}
-	return output;
-}
-
 void str_replace(std::string &str, char from, char to)
 {
 	std::replace(str.begin(), str.end(), from, to);
diff --git a/src/util/string.h b/src/util/string.h
index 9e59ab20a..40ef3e4d3 100644
--- a/src/util/string.h
+++ b/src/util/string.h
@@ -386,14 +386,6 @@ inline void str_replace(std::string &str, const std::string &pattern,
 	}
 }
 
-/**
- * Remove all chat escape sequences in \p s.
- *
- * @param s The string in which to remove escape sequences.
- * @return \p s, with escape sequences removed.
- */
-std::wstring removeChatEscapes(const std::wstring &s);
-
 /**
  * Replace all occurrences of the character \p from in \p str with \p to.
  *
@@ -476,7 +468,7 @@ inline std::string wrap_rows(const std::string &from,
  * Removes backslashes from an escaped string (FormSpec strings)
  */
 template <typename T>
-inline std::basic_string<T> unescape_string(std::basic_string<T> &s)
+inline std::basic_string<T> unescape_string(const std::basic_string<T> &s)
 {
 	std::basic_string<T> res;
 
@@ -492,6 +484,40 @@ inline std::basic_string<T> unescape_string(std::basic_string<T> &s)
 	return res;
 }
 
+/**
+ * Remove all escape sequences in \p s.
+ *
+ * @param s The string in which to remove escape sequences.
+ * @return \p s, with escape sequences removed.
+ */
+template <typename T>
+std::basic_string<T> unescape_enriched(const std::basic_string<T> &s)
+{
+	std::basic_string<T> output;
+	size_t i = 0;
+	while (i < s.length()) {
+		if (s[i] == '\x1b') {
+			++i;
+			if (i == s.length()) continue;
+			if (s[i] == '(') {
+				++i;
+				while (i < s.length() && s[i] != ')') {
+					if (s[i] == '\\') {
+						++i;
+					}
+					++i;
+				}
+				++i;
+			} else {
+				++i;
+			}
+			continue;
+		}
+		output += s[i];
+		++i;
+	}
+	return output;
+}
 
 /**
  * Checks that all characters in \p to_check are a decimal digits.
-- 
GitLab