From 81536c329b3e2b8b22a94fa456167ff340a14e82 Mon Sep 17 00:00:00 2001 From: SuperCake Date: Tue, 28 Apr 2026 02:20:55 +0200 Subject: [PATCH 1/2] fix(CheckChatText): avoid splitting utf-8 code points on truncation --- src/game/server/client.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/game/server/client.cpp b/src/game/server/client.cpp index 4794c6da9..d8bb54dba 100644 --- a/src/game/server/client.cpp +++ b/src/game/server/client.cpp @@ -73,7 +73,12 @@ char * CheckChatText( CBasePlayer *pPlayer, char *text ) // cut off after P_MAX_LEN chars if ( length > P_MAX_LEN ) - p[P_MAX_LEN] = 0; + { + // don't split utf-8 code point + size_t i = P_MAX_LEN; + while( i > 0 && ( static_cast(p[i]) & 0b1100'0000 ) == 0b1000'0000 ) --i; + p[i] = '\0'; + } GameRules()->CheckChatText( pPlayer, p ); From cd6a1a8d7387ad0c36258808c49f06d91d7a7ff4 Mon Sep 17 00:00:00 2001 From: SuperCake Date: Tue, 28 Apr 2026 15:14:21 +0200 Subject: [PATCH 2/2] fix(chat): prevent input exceeding utf-8 byte limit Because of the `Send` method, max char limit should be based on utf-8 byte count, not utf-16 chars. --- src/game/client/hud_basechat.cpp | 58 ++++++++++++++++++++++++++++++++ src/game/client/hud_basechat.h | 5 +++ 2 files changed, 63 insertions(+) diff --git a/src/game/client/hud_basechat.cpp b/src/game/client/hud_basechat.cpp index 89a05d720..daa3446d8 100644 --- a/src/game/client/hud_basechat.cpp +++ b/src/game/client/hud_basechat.cpp @@ -466,6 +466,8 @@ CBaseHudChatInputLine::CBaseHudChatInputLine( CBaseHudChat *parent, char const * m_pPrompt = new vgui::Label( this, "ChatInputPrompt", L"Enter text:" ); m_pInput = new CBaseHudChatEntry( this, "ChatInput", parent ); m_pInput->SetMaximumCharCount( 127 ); + // Send converts text to utf-8 + m_pInput->m_iMaxByteCount = 127; } void CBaseHudChatInputLine::ApplySchemeSettings(vgui::IScheme *pScheme) @@ -2519,3 +2521,59 @@ void CBaseHudChat::FireGameEvent( IGameEvent *event ) ChatPrintf( player->entindex(), CHAT_FILTER_NONE, "(SourceTV) %s", event->GetString( "text" ) ); } } + +// Prevent player from inserting text over utf-8 byte limit +void CBaseHudChatEntry::InsertChar(wchar_t ch) +{ + if ( m_iMaxByteCount == -1 ) + { + BaseClass::InsertChar(ch); + return; + } + + // single utf-16 char converted to utf-8 is 3 byte long in worst case + const int iBufLen = BaseClass::GetTextLength() * 3; + + // Shortcut: fitting max byte count even in worst case + if ( iBufLen + 4 <= m_iMaxByteCount ) + { + BaseClass::InsertChar(ch); + return; + } + + // Count bytes of converted utf-8 str + m_szCharBuf.EnsureCapacity( iBufLen ); + BaseClass::GetText( m_szCharBuf.Base(), m_szCharBuf.Count() ); + const int iCurrentByteLen = Q_strlen( m_szCharBuf.Base() ); + + // Shortcut: average case + if ( iCurrentByteLen + 4 <= m_iMaxByteCount ) + { + BaseClass::InsertChar(ch); + return; + } + + // Edge case: check if current wchar_t will cause Send to truncate message + int iCharByteLen; // utf-8 + if ( ch < 0x80 ) iCharByteLen = 1; + else if ( ch < 0x800 ) iCharByteLen = 2; + else if ( ch >= 0xD800 && ch <= 0xDBFF ) iCharByteLen = 4; // high surrogate, assume pair + else if ( ch >= 0xDC00 && ch <= 0xDFFF ) // low surrogate + { + // Check if previous wchar was a high surrogate + wchar_t wszHigh[2]; + const int iLast = BaseClass::GetTextLength() - 1; + BaseClass::GetTextRange( wszHigh, iLast, 1 ); + if ( wszHigh[0] >= 0xD800 && wszHigh[0] <= 0xDBFF ) { BaseClass::InsertChar(ch); return; } + else return; // it was not a hight surrogate, reject + } + else iCharByteLen = 3; + + if ( iCurrentByteLen + iCharByteLen <= m_iMaxByteCount ) + { + BaseClass::InsertChar(ch); + return; + } + + return; // do not insert anything +} diff --git a/src/game/client/hud_basechat.h b/src/game/client/hud_basechat.h index e453d89bc..b74e5f91e 100644 --- a/src/game/client/hud_basechat.h +++ b/src/game/client/hud_basechat.h @@ -324,6 +324,8 @@ class CBaseHudChatEntry : public vgui::TextEntry { typedef vgui::TextEntry BaseClass; public: + int m_iMaxByteCount = -1; // utf-8 string length + CBaseHudChatEntry( vgui::Panel *parent, char const *panelName, CBaseHudChat *pChat ) : BaseClass( parent, panelName ) { @@ -340,6 +342,8 @@ class CBaseHudChatEntry : public vgui::TextEntry SetPaintBorderEnabled( false ); } + virtual void InsertChar(wchar_t ch); + virtual void OnKeyCodeTyped(vgui::KeyCode code) { if ( code == KEY_ENTER || code == KEY_PAD_ENTER || code == KEY_ESCAPE ) @@ -371,6 +375,7 @@ class CBaseHudChatEntry : public vgui::TextEntry private: CBaseHudChat *m_pHudChat; + CUtlMemory m_szCharBuf{ 0, 64 }; // tmp buffer }; //-----------------------------------------------------------------------------