diff --git a/mysql-test/main/lowercase_table.result b/mysql-test/main/lowercase_table.result index 49468793d02..ff0d0f81b24 100644 --- a/mysql-test/main/lowercase_table.result +++ b/mysql-test/main/lowercase_table.result @@ -164,3 +164,30 @@ delete from mysql.proc where name = ''; # # End of 10.3 tests # +# +# Start of 11.3 tests +# +# +# MDEV-31948 Add class DBNameBuffer, split check_db_name() into stages +# +SET NAMES utf8; +SET @mb3char= _utf8 0xEFBFAD; +EXECUTE IMMEDIATE CONCAT('use `', REPEAT(@mb3char, 64), '`'); +ERROR 42000: Unknown database '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■' +EXECUTE IMMEDIATE CONCAT('use `#mysql50#', REPEAT(@mb3char, 64), '`'); +ERROR 42000: Unknown database '#mysql50#■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■' +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `', REPEAT(@mb3char, 64), '`'); +ERROR 42000: Unknown database '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■' +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `#mysql50#', REPEAT(@mb3char, 64), '`'); +ERROR 42000: Unknown database '#mysql50#■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■' +EXECUTE IMMEDIATE CONCAT('use `', REPEAT(@mb3char, 65), '`'); +ERROR 42000: Incorrect database name '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■...' +EXECUTE IMMEDIATE CONCAT('use `#mysql50#', REPEAT(@mb3char, 65), '`'); +ERROR 42000: Incorrect database name '#mysql50#■■■■■■■■■■■■■■■■■■■■■■■■■■■■■...' +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `', REPEAT(@mb3char, 65), '`'); +ERROR 42000: Incorrect database name '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■...' +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `#mysql50#', REPEAT(@mb3char, 65), '`'); +ERROR 42000: Incorrect database name '#mysql50#■■■■■■■■■■■■■■■■■■■■■■■■■■■■■...' +# +# End of 11.3 tests +# diff --git a/mysql-test/main/lowercase_table.test b/mysql-test/main/lowercase_table.test index 8d493fff5cd..249989060e0 100644 --- a/mysql-test/main/lowercase_table.test +++ b/mysql-test/main/lowercase_table.test @@ -142,3 +142,43 @@ delete from mysql.proc where name = ''; --echo # --echo # End of 10.3 tests --echo # + +--echo # +--echo # Start of 11.3 tests +--echo # + +--echo # +--echo # MDEV-31948 Add class DBNameBuffer, split check_db_name() into stages +--echo # + +SET NAMES utf8; + +# U+FFED HALFWIDTH BLACK SQUARE +SET @mb3char= _utf8 0xEFBFAD; + +# Database names fitting into the NAME_CHAR_LEN characters limit + +--error ER_BAD_DB_ERROR +EXECUTE IMMEDIATE CONCAT('use `', REPEAT(@mb3char, 64), '`'); +--error ER_BAD_DB_ERROR +EXECUTE IMMEDIATE CONCAT('use `#mysql50#', REPEAT(@mb3char, 64), '`'); +--error ER_BAD_DB_ERROR +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `', REPEAT(@mb3char, 64), '`'); +--error ER_BAD_DB_ERROR +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `#mysql50#', REPEAT(@mb3char, 64), '`'); + + +# Database names longer than NAME_CHAR_LEN characters + +--error ER_WRONG_DB_NAME +EXECUTE IMMEDIATE CONCAT('use `', REPEAT(@mb3char, 65), '`'); +--error ER_WRONG_DB_NAME +EXECUTE IMMEDIATE CONCAT('use `#mysql50#', REPEAT(@mb3char, 65), '`'); +--error ER_WRONG_DB_NAME +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `', REPEAT(@mb3char, 65), '`'); +--error ER_WRONG_DB_NAME +EXECUTE IMMEDIATE CONCAT('SHOW CREATE DATABASE `#mysql50#', REPEAT(@mb3char, 65), '`'); + +--echo # +--echo # End of 11.3 tests +--echo # diff --git a/sql/char_buffer.h b/sql/char_buffer.h new file mode 100644 index 00000000000..344d3f24011 --- /dev/null +++ b/sql/char_buffer.h @@ -0,0 +1,72 @@ +#ifndef CHAR_BUFFER_INCLUDED +#define CHAR_BUFFER_INCLUDED +/* + Copyright (c) 2023, MariaDB + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; version 2 of + the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA +*/ + + +/* + A string buffer with length. + This template class is useful to store things like database, table names, + whose maximum length a small fixed known value. Mainly to be used as + stack variables to store temporary values. + + Can store exact string copies or casefolded string copies. + The stored value is returned as a LEX_CSTRING. +*/ +template +class CharBuffer +{ + char m_buff[buff_sz + 1 /* one extra byte for '\0' */]; + size_t m_length; +public: + CharBuffer() + :m_length(0) + { + m_buff[0]= '\0'; + } + CharBuffer & copy_bin(const LEX_CSTRING &str) + { + m_length= MY_MIN(buff_sz, str.length); + memcpy(m_buff, str.str, m_length); + m_buff[m_length]= '\0'; + return *this; + } + CharBuffer & copy_casedn(CHARSET_INFO *cs, const LEX_CSTRING &str) + { + m_length= cs->cset->casedn(cs, str.str, str.length, m_buff, buff_sz); + /* + casedn() never writes outsize of buff_sz (unless a bug in casedn()), + so it's safe to write '\0' at the position m_length: + */ + DBUG_ASSERT(m_length <= buff_sz); + m_buff[m_length]= '\0'; + return *this; + } + CharBuffer & copy_casedn(CHARSET_INFO *cs, const LEX_CSTRING &str, + bool casedn) + { + casedn ? copy_casedn(cs, str) : copy_bin(str); + return *this; + } + LEX_CSTRING to_lex_cstring() const + { + return LEX_CSTRING{m_buff, m_length}; + } +}; + +#endif // CHAR_BUFFER_INCLUDED diff --git a/sql/lex_ident.h b/sql/lex_ident.h new file mode 100644 index 00000000000..1c060325190 --- /dev/null +++ b/sql/lex_ident.h @@ -0,0 +1,70 @@ +#ifndef LEX_IDENT_INCLUDED +#define LEX_IDENT_INCLUDED +/* + Copyright (c) 2023, MariaDB + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; version 2 of + the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA +*/ + +#include "char_buffer.h" + + +/* + Identifiers for the database objects stored on disk, + e.g. databases, tables, triggers. +*/ +class Lex_ident_fs: public LEX_CSTRING +{ +public: + Lex_ident_fs() + :LEX_CSTRING({0,0}) + { } + Lex_ident_fs(const char *str, size_t length) + :LEX_CSTRING({str, length}) + { } + explicit Lex_ident_fs(const LEX_CSTRING &str) + :LEX_CSTRING(str) + { } + static bool check_body(const char *name, size_t length, + bool disallow_path_chars); + bool check_db_name() const; + bool check_db_name_with_error() const; +}; + + +/* + A helper class to store temporary database names in a buffer. + After constructing it's typically should be checked using + Lex_ident_fs::check_db_name(). + + Note, the database name passed to the constructor can originally + come from the parser and can be of an atribtrary long length. + Let's reserve additional buffer space for one extra character + (SYSTEM_CHARSET_MBMAXLEN bytes), so check_db_name() can still + detect too long names even if the constructor cuts the data. +*/ +class DBNameBuffer: public CharBuffer +{ +public: + DBNameBuffer() + { } + DBNameBuffer(const LEX_CSTRING &db, bool casedn) + { + copy_casedn(&my_charset_utf8mb3_general_ci, db, casedn); + } +}; + + +#endif // LEX_IDENT_INCLUDED diff --git a/sql/sql_class.h b/sql/sql_class.h index f5d220aa13d..342c39a143c 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -24,6 +24,7 @@ #include "dur_prop.h" #include #include "sql_const.h" +#include "lex_ident.h" #include "sql_used.h" #include #include "log.h" diff --git a/sql/sql_db.cc b/sql/sql_db.cc index 452a01773e5..5a3bbda5b3e 100644 --- a/sql/sql_db.cc +++ b/sql/sql_db.cc @@ -1697,6 +1697,7 @@ static void backup_current_db_name(THD *thd, uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, bool force_switch) { + DBNameBuffer new_db_buff; LEX_CSTRING new_db_file_name; Security_context *sctx= thd->security_ctx; @@ -1739,19 +1740,10 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, goto done; } - /* - Now we need to make a copy because check_db_name requires a - non-constant argument. Actually, it takes database file name. - - TODO: fix check_db_name(). - */ - - new_db_file_name.str= my_strndup(key_memory_THD_db, new_db_name->str, - new_db_name->length, MYF(MY_WME)); - new_db_file_name.length= new_db_name->length; - - if (new_db_file_name.str == NULL) - DBUG_RETURN(ER_OUT_OF_RESOURCES); /* the error is set */ + new_db_file_name= lower_case_table_names ? + new_db_buff.copy_casedn(&my_charset_utf8mb3_general_ci, + *new_db_name).to_lex_cstring() : + *new_db_name; /* NOTE: if check_db_name() fails, we should throw an error in any case, @@ -1763,11 +1755,8 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, The cast below ok here as new_db_file_name was just allocated */ - if (check_db_name((LEX_STRING*) &new_db_file_name)) + if (Lex_ident_fs(new_db_file_name).check_db_name_with_error()) { - my_error(ER_WRONG_DB_NAME, MYF(0), new_db_file_name.str); - my_free(const_cast(new_db_file_name.str)); - if (force_switch) mysql_change_db_impl(thd, NULL, NO_ACL, thd->variables.collation_server); @@ -1797,7 +1786,6 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, new_db_file_name.str); general_log_print(thd, COM_INIT_DB, ER_THD(thd, ER_DBACCESS_DENIED_ERROR), sctx->priv_user, sctx->priv_host, new_db_file_name.str); - my_free(const_cast(new_db_file_name.str)); DBUG_RETURN(ER_DBACCESS_DENIED_ERROR); } #endif @@ -1814,8 +1802,6 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, ER_BAD_DB_ERROR, ER_THD(thd, ER_BAD_DB_ERROR), new_db_file_name.str); - my_free(const_cast(new_db_file_name.str)); - /* Change db to NULL. */ mysql_change_db_impl(thd, NULL, NO_ACL, thd->variables.collation_server); @@ -1828,7 +1814,6 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, /* Report an error and free new_db_file_name. */ my_error(ER_BAD_DB_ERROR, MYF(0), new_db_file_name.str); - my_free(const_cast(new_db_file_name.str)); /* The operation failed. */ @@ -1836,14 +1821,23 @@ uint mysql_change_db(THD *thd, const LEX_CSTRING *new_db_name, } } + db_default_cl= get_default_db_collation(thd, new_db_file_name.str); + /* + new_db_file_name allocated memory on the stack. + mysql_change_db_impl() expects a my_alloc-ed memory. NOTE: in mysql_change_db_impl() new_db_file_name is assigned to THD attributes and will be freed in THD::~THD(). */ - - db_default_cl= get_default_db_collation(thd, new_db_file_name.str); - - mysql_change_db_impl(thd, &new_db_file_name, db_access, db_default_cl); + if (const char *tmp= my_strndup(key_memory_THD_db, + new_db_file_name.str, + new_db_file_name.length, MYF(MY_WME))) + { + LEX_CSTRING new_db_malloced({tmp, new_db_file_name.length}); + mysql_change_db_impl(thd, &new_db_malloced, db_access, db_default_cl); + } + else + DBUG_RETURN(ER_OUT_OF_RESOURCES); /* the error is set */ done: thd->session_tracker.current_schema.mark_as_changed(thd); diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 11bca673aae..8b9cd7c753f 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -6265,21 +6265,14 @@ static bool generate_incident_event(THD *thd) static int __attribute__ ((noinline)) show_create_db(THD *thd, LEX *lex) { - char db_name_buff[NAME_LEN+1]; - LEX_CSTRING db_name; DBUG_EXECUTE_IF("4x_server_emul", my_error(ER_UNKNOWN_ERROR, MYF(0)); return 1;); - db_name.str= db_name_buff; - db_name.length= lex->name.length; - strmov(db_name_buff, lex->name.str); - - if (check_db_name((LEX_STRING*) &db_name)) - { - my_error(ER_WRONG_DB_NAME, MYF(0), db_name.str); + DBNameBuffer dbbuf(lex->name, lower_case_table_names); + if (Lex_ident_fs(dbbuf.to_lex_cstring()).check_db_name_with_error()) return 1; - } - return mysqld_show_create_db(thd, &db_name, &lex->name, lex->create_info); + LEX_CSTRING db= dbbuf.to_lex_cstring(); + return mysqld_show_create_db(thd, &db, &lex->name, lex->create_info); } diff --git a/sql/table.cc b/sql/table.cc index 993ee347ad4..5cba8eb5390 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -5240,10 +5240,6 @@ bool check_db_name(LEX_STRING *org_name) bool check_table_name(const char *name, size_t length, bool disallow_path_chars) { - // name length in symbols - size_t char_length= 0; - const char *end= name+length; - if (!disallow_path_chars && (disallow_path_chars= check_mysql50_prefix(name))) { @@ -5251,8 +5247,20 @@ bool check_table_name(const char *name, size_t length, bool disallow_path_chars) length-= MYSQL50_TABLE_NAME_PREFIX_LENGTH; } + return Lex_ident_fs::check_body(name, length, disallow_path_chars); +} + + +bool Lex_ident_fs::check_body(const char *name, size_t length, + bool disallow_path_chars) +{ if (!length || length > NAME_LEN) return 1; + + // name length in symbols + size_t char_length= 0; + const char *end= name + length; + #if defined(USE_MB) && defined(USE_MB_IDENT) bool last_char_is_space= FALSE; #else @@ -5302,6 +5310,42 @@ bool check_table_name(const char *name, size_t length, bool disallow_path_chars) } +/** + Check if the name is a valid database name + @returns false - on success (valid) + @returns true - on error (invalid) +*/ +bool Lex_ident_fs::check_db_name() const +{ + DBUG_ASSERT(str); + if (check_mysql50_prefix(str)) + { + Lex_ident_fs name(Lex_cstring(str + MYSQL50_TABLE_NAME_PREFIX_LENGTH, + length - MYSQL50_TABLE_NAME_PREFIX_LENGTH)); + return db_name_is_in_ignore_db_dirs_list(name.str) || + check_body(name.str, name.length, true); + } + return db_name_is_in_ignore_db_dirs_list(str) || + check_body(str, length, false); +} + + +/** + Check if the name is a valid database name + and raise an error in case of an invalid name. + + @returns false - on success (valid) + @returns true - on error (invalid) +*/ +bool Lex_ident_fs::check_db_name_with_error() const +{ + if (!check_db_name()) + return false; + my_error(ER_WRONG_DB_NAME ,MYF(0), safe_str(str)); + return true; +} + + bool check_column_name(const char *name) { // name length in symbols