@@ -86,6 +86,93 @@ DBType GetOurType(enum_field_types type)
8686 return DBType_Unknown;
8787}
8888
89+ // Reimplements mysql_cset_escape_slashes() for charsets where no byte of an mb
90+ // sequence can be below 0x80 which makes escaping independent of the connection.
91+ static size_t EscapeStringBackslashes (char *to, const char *from, size_t length)
92+ {
93+ char *start = to;
94+
95+ for (const char *end = from + length; from < end; from++)
96+ {
97+ char esc = ' \0 ' ;
98+
99+ switch (*from)
100+ {
101+ case 0 :
102+ esc = ' 0' ;
103+ break ;
104+ case ' \n ' :
105+ esc = ' n' ;
106+ break ;
107+ case ' \r ' :
108+ esc = ' r' ;
109+ break ;
110+ case ' \\ ' :
111+ case ' \' ' :
112+ case ' "' :
113+ esc = *from;
114+ break ;
115+ case ' \032 ' :
116+ esc = ' Z' ;
117+ break ;
118+ }
119+
120+ if (esc)
121+ {
122+ *to++ = ' \\ ' ;
123+ *to++ = esc;
124+ }
125+ else
126+ {
127+ *to++ = *from;
128+ }
129+ }
130+
131+ *to = ' \0 ' ;
132+
133+ return (size_t )(to - start);
134+ }
135+
136+ static bool IsByteWiseEscapable (const MY_CHARSET_INFO &cs)
137+ {
138+ if (cs.mbmaxlen <= 1 )
139+ {
140+ return true ;
141+ }
142+
143+ // big5, gbk, sjis, etc. can end a sequence in 0x5c; UTF-8 can't.
144+ return cs.csname != NULL
145+ && (strcmp (cs.csname , " utf8" ) == 0
146+ || strcmp (cs.csname , " utf8mb3" ) == 0
147+ || strcmp (cs.csname , " utf8mb4" ) == 0 );
148+ }
149+
150+ // Caches whether QuoteString() can escape without the connection. Must be called
151+ // with m_FullLock held.
152+ void MyDatabase::RefreshEscapeContext ()
153+ {
154+ if (m_bNoBackslashEscapes)
155+ {
156+ return ;
157+ }
158+
159+ // Escaping a lone backslash reveals whether the server has NO_BACKSLASH_ESCAPES
160+ // without reading server_status out of the connection struct. Check it, because
161+ // sql_mode can change while a query is in flight, and mishandling here could
162+ // allow for injection.
163+ char probe[3 ];
164+ if (mysql_real_escape_string (m_mysql, probe, " \\ " , 1 ) != 2 )
165+ {
166+ m_bNoBackslashEscapes = true ;
167+ m_bCanEscapeLocally = false ;
168+ return ;
169+ }
170+
171+ MY_CHARSET_INFO cs;
172+ mysql_get_character_set_info (m_mysql, &cs);
173+ m_bCanEscapeLocally = IsByteWiseEscapable (cs);
174+ }
175+
89176MyDatabase::MyDatabase (MYSQL *mysql, const DatabaseInfo *info, bool persistent)
90177: m_mysql(mysql), m_bPersistent(persistent)
91178{
@@ -102,6 +189,9 @@ MyDatabase::MyDatabase(MYSQL *mysql, const DatabaseInfo *info, bool persistent)
102189 m_Info.maxTimeout = info->maxTimeout ;
103190 m_Info.port = info->port ;
104191
192+ // Nothing else can reach the connection yet, so no lock is needed here.
193+ RefreshEscapeContext ();
194+
105195 // DBI, for historical reasons, guarantees an initial refcount of 1.
106196 AddRef ();
107197}
@@ -157,8 +247,6 @@ const char *MyDatabase::GetError(int *errCode)
157247
158248bool MyDatabase::QuoteString (const char *str, char buffer[], size_t maxlength, size_t *newSize)
159249{
160- std::lock_guard<std::recursive_mutex> lock (m_FullLock);
161-
162250 unsigned long size = static_cast <unsigned long >(strlen (str));
163251 unsigned long needed = size * 2 + 1 ;
164252
@@ -171,7 +259,24 @@ bool MyDatabase::QuoteString(const char *str, char buffer[], size_t maxlength, s
171259 return false ;
172260 }
173261
174- needed = mysql_real_escape_string (m_mysql, buffer, str, size);
262+ // Refresh only while the connection is idle; blocking would stall the game
263+ // thread for as long as a threaded query takes to come back.
264+ if (m_FullLock.try_lock ())
265+ {
266+ RefreshEscapeContext ();
267+ m_FullLock.unlock ();
268+ }
269+
270+ if (m_bCanEscapeLocally)
271+ {
272+ needed = static_cast <unsigned long >(EscapeStringBackslashes (buffer, str, size));
273+ }
274+ else
275+ {
276+ std::lock_guard<std::recursive_mutex> lock (m_FullLock);
277+ needed = mysql_real_escape_string (m_mysql, buffer, str, size);
278+ }
279+
175280 if (newSize)
176281 {
177282 *newSize = (size_t )needed;
@@ -313,6 +418,10 @@ bool MyDatabase::SetCharacterSet(const char *characterset)
313418 bool res;
314419 LockForFullAtomicOperation ();
315420 res = mysql_set_character_set (m_mysql, characterset) == 0 ? true : false ;
421+ if (res)
422+ {
423+ RefreshEscapeContext ();
424+ }
316425 UnlockFromFullAtomicOperation ();
317426 return res;
318427}
0 commit comments