From 97c74323d8d2eebf064379adbe4cbc7c95e5fe67 Mon Sep 17 00:00:00 2001 From: Toes | Robin Ury <1146921+binury@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:58:25 -0400 Subject: [PATCH 1/5] Change: empty check to reject all whitespace kinds --- ui/chat_ui.gd | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 0e94e15..b157d70 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -893,8 +893,9 @@ func _on_entry_text_submitted(_value: String) -> void: func _send() -> void: if _send_pending: return - var body := _entry.text - if body.strip_edges().is_empty(): + var strippable_spaces := RegEx.create_from_string("(^\\s+|\\s+$)") + var body := strippable_spaces.sub(_entry.text, "", true) + if body.is_empty(): close_chat() return if _handle_editor_world_command(body): From 00ec796c7a685d455f5a1e9622b4af5b79a78e15 Mon Sep 17 00:00:00 2001 From: Toes | Robin Ury <1146921+binury@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:38:04 -0400 Subject: [PATCH 2/5] Refactor: chat sanitization into NetworkChat proc --- network/network_chat_protocol.gd | 4 ++-- ui/chat_ui.gd | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/network/network_chat_protocol.gd b/network/network_chat_protocol.gd index ef7294d..6538295 100644 --- a/network/network_chat_protocol.gd +++ b/network/network_chat_protocol.gd @@ -15,8 +15,8 @@ enum Kind { PLAYER, SYSTEM } static func sanitize_body(value: Variant) -> String: if typeof(value) != TYPE_STRING: return "" - var result: String = str(value).replace("\r", " ").replace("\n", " ") - result = result.replace("\t", " ").strip_edges() + var strippable_whitespace = RegEx.create_from_string("^[\\s\\p{Z}\\p{Cf}\\x{2800}]+|[\\s\\p{Z}\\p{Cf}\\x{2800}]+$") + var result := strippable_whitespace.sub(value, "") if result.is_empty() or result.length() > MAX_VISIBLE_CHARACTERS: return "" if result.to_utf8_buffer().size() > MAX_UTF8_BYTES: diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index b157d70..0b26ffb 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -893,15 +893,14 @@ func _on_entry_text_submitted(_value: String) -> void: func _send() -> void: if _send_pending: return - var strippable_spaces := RegEx.create_from_string("(^\\s+|\\s+$)") - var body := strippable_spaces.sub(_entry.text, "", true) + var body := NetworkChatProtocol.sanitize_body(_entry.text) if body.is_empty(): close_chat() return if _handle_editor_world_command(body): return _send_pending = true - _pending_send_body = NetworkChatProtocol.sanitize_body(body) + _pending_send_body = body _entry.editable = false if not _service.send_local_message( body, From b1061f0165cbd27c9f957bb10d779c6a73aaddd3 Mon Sep 17 00:00:00 2001 From: Toes | Robin Ury <1146921+binury@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:18:03 -0400 Subject: [PATCH 3/5] Refactor: invert whitespace stripping pattern --- network/network_chat_protocol.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/network_chat_protocol.gd b/network/network_chat_protocol.gd index 6538295..389260d 100644 --- a/network/network_chat_protocol.gd +++ b/network/network_chat_protocol.gd @@ -15,7 +15,7 @@ enum Kind { PLAYER, SYSTEM } static func sanitize_body(value: Variant) -> String: if typeof(value) != TYPE_STRING: return "" - var strippable_whitespace = RegEx.create_from_string("^[\\s\\p{Z}\\p{Cf}\\x{2800}]+|[\\s\\p{Z}\\p{Cf}\\x{2800}]+$") + var strippable_whitespace = RegEx.create_from_string("^[^\\w]+|[^\\w]+$") var result := strippable_whitespace.sub(value, "") if result.is_empty() or result.length() > MAX_VISIBLE_CHARACTERS: return "" From c20bb35929a98622a55b73363558f8104f6aba6d Mon Sep 17 00:00:00 2001 From: Toes | Robin Ury <1146921+binury@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:19:34 -0400 Subject: [PATCH 4/5] Add(test): chat validation --- scripts/run_validations.sh | 1 + tests/chat_validation.gd | 21 +++++++++++++++++++++ tests/chat_validation.gd.uid | 1 + 3 files changed, 23 insertions(+) create mode 100644 tests/chat_validation.gd create mode 100644 tests/chat_validation.gd.uid diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 7c2eb77..6bf5b1f 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -15,6 +15,7 @@ readonly -a QUICK_TESTS=( "tests/android_readiness_validation.gd" "tests/camera_drag_validation.gd" "tests/character_rig_validation.gd" + "tests/chat_validation.gd" "tests/controller_focus_presentation_validation.gd" "tests/controller_menu_accessibility_validation.gd" "tests/controller_mapping_validation.gd" diff --git a/tests/chat_validation.gd b/tests/chat_validation.gd new file mode 100644 index 0000000..aed8f17 --- /dev/null +++ b/tests/chat_validation.gd @@ -0,0 +1,21 @@ +extends SceneTree + +const MainScene: PackedScene = preload("res://main/main.tscn") + +func _initialize() -> void: + call_deferred("_run") + +func _run() -> void: + ## Regular whitespace should be stripped + assert(NetworkChatProtocol.sanitize_body(" a") == "a", "Leading space not trimmed") + assert(NetworkChatProtocol.sanitize_body("b ") == "b", "Trailing space not trimmed") + assert(NetworkChatProtocol.sanitize_body("a b") == "a b", "Innocent whitespace culled") + + ## Some examples of Unicode whitespace that should be stripped + const em_space := " " # U+2003 EM SPACE + assert(NetworkChatProtocol.sanitize_body(em_space + "a") == "a") + const braille_space := "⠀" # U+2800 BRAILLE PATTERN BLANK + assert(NetworkChatProtocol.sanitize_body(braille_space + "a") == "a") + + print("Chat validation: PASS") + quit(0) diff --git a/tests/chat_validation.gd.uid b/tests/chat_validation.gd.uid new file mode 100644 index 0000000..54e6823 --- /dev/null +++ b/tests/chat_validation.gd.uid @@ -0,0 +1 @@ +uid://ohemhv2sf5g4 From 91b54e4a01515416465ce63fce0dc222f4e6d9e5 Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 21 Aug 2026 09:23:28 -0400 Subject: [PATCH 5/5] Fix Unicode chat sanitization --- network/network_chat_protocol.gd | 18 ++++++++++++--- tests/chat_validation.gd | 39 +++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/network/network_chat_protocol.gd b/network/network_chat_protocol.gd index 389260d..2e91432 100644 --- a/network/network_chat_protocol.gd +++ b/network/network_chat_protocol.gd @@ -9,15 +9,27 @@ const MAX_HISTORY: int = 100 const LATE_JOIN_HISTORY: int = 50 const MAX_ID_LENGTH: int = 96 +static var _invisible_edge_pattern: RegEx = RegEx.create_from_string( + "^[\\s\\p{Z}\\p{Cf}\\x{2800}]+|[\\s\\p{Z}\\p{Cf}\\x{2800}]+$" +) +static var _visible_content_pattern: RegEx = RegEx.create_from_string( + "[^\\s\\p{Z}\\p{Cf}\\x{2800}]" +) + enum Kind { PLAYER, SYSTEM } static func sanitize_body(value: Variant) -> String: if typeof(value) != TYPE_STRING: return "" - var strippable_whitespace = RegEx.create_from_string("^[^\\w]+|[^\\w]+$") - var result := strippable_whitespace.sub(value, "") - if result.is_empty() or result.length() > MAX_VISIBLE_CHARACTERS: + var result: String = str(value).replace("\r", " ").replace("\n", " ") + result = result.replace("\t", " ") + result = _invisible_edge_pattern.sub(result, "", true) + if ( + result.is_empty() + or _visible_content_pattern.search(result) == null + or result.length() > MAX_VISIBLE_CHARACTERS + ): return "" if result.to_utf8_buffer().size() > MAX_UTF8_BYTES: return "" diff --git a/tests/chat_validation.gd b/tests/chat_validation.gd index aed8f17..fa0cd8e 100644 --- a/tests/chat_validation.gd +++ b/tests/chat_validation.gd @@ -1,21 +1,38 @@ extends SceneTree -const MainScene: PackedScene = preload("res://main/main.tscn") - func _initialize() -> void: call_deferred("_run") -func _run() -> void: - ## Regular whitespace should be stripped - assert(NetworkChatProtocol.sanitize_body(" a") == "a", "Leading space not trimmed") - assert(NetworkChatProtocol.sanitize_body("b ") == "b", "Trailing space not trimmed") - assert(NetworkChatProtocol.sanitize_body("a b") == "a b", "Innocent whitespace culled") - ## Some examples of Unicode whitespace that should be stripped +func _run() -> void: + assert(NetworkChatProtocol.sanitize_body(42).is_empty()) + assert(NetworkChatProtocol.sanitize_body(" a ") == "a") + assert(NetworkChatProtocol.sanitize_body("a b") == "a b") + assert(NetworkChatProtocol.sanitize_body("a\tb\nc") == "a b c") + assert(NetworkChatProtocol.sanitize_body(" !hello!? ") == "!hello!?") + assert(NetworkChatProtocol.sanitize_body("!!!") == "!!!") + const em_space := " " # U+2003 EM SPACE - assert(NetworkChatProtocol.sanitize_body(em_space + "a") == "a") - const braille_space := "⠀" # U+2800 BRAILLE PATTERN BLANK - assert(NetworkChatProtocol.sanitize_body(braille_space + "a") == "a") + const zero_width_space := "​" # U+200B ZERO WIDTH SPACE + const braille_blank := "⠀" # U+2800 BRAILLE PATTERN BLANK + const invisible_padding := em_space + zero_width_space + braille_blank + assert( + NetworkChatProtocol.sanitize_body(invisible_padding + "a" + invisible_padding) + == "a" + ) + assert(NetworkChatProtocol.sanitize_body(invisible_padding).is_empty()) + assert(NetworkChatProtocol.sanitize_body(" \t\n" + invisible_padding).is_empty()) + + assert( + NetworkChatProtocol.sanitize_body("a".repeat( + NetworkChatProtocol.MAX_VISIBLE_CHARACTERS + )).length() == NetworkChatProtocol.MAX_VISIBLE_CHARACTERS + ) + assert( + NetworkChatProtocol.sanitize_body("a".repeat( + NetworkChatProtocol.MAX_VISIBLE_CHARACTERS + 1 + )).is_empty() + ) print("Chat validation: PASS") quit(0)