From bf27133ffc06765987417650acb090fd11d6906c Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 1 Jun 2022 19:17:43 +0200 Subject: [PATCH] enhanced escaping of special characters Enhanced escaping of special characters in the schema builder. - added note to the docs - added unit tests --- docs/database/migrations.mdx | 30 ++++++++-------- docs/supported-compilers.mdx | 2 +- .../schema/grammars/mysqlschemagrammar.hpp | 4 +-- .../schema/grammars/postgresschemagrammar.hpp | 3 ++ include/orm/schema/grammars/schemagrammar.hpp | 3 ++ .../schema/grammars/sqliteschemagrammar.hpp | 3 ++ .../schema/grammars/mysqlschemagrammar.cpp | 35 ++++++++++++------- .../schema/grammars/postgresschemagrammar.cpp | 23 ++++++++---- src/orm/schema/grammars/schemagrammar.cpp | 2 +- .../schema/grammars/sqliteschemagrammar.cpp | 5 +++ .../tst_mysql_schemabuilder.cpp | 28 +++++++++++++++ .../tst_postgresql_schemabuilder.cpp | 27 ++++++++++++++ 12 files changed, 128 insertions(+), 37 deletions(-) diff --git a/docs/database/migrations.mdx b/docs/database/migrations.mdx index 0c8d31ef2..8fbfa4c9d 100644 --- a/docs/database/migrations.mdx +++ b/docs/database/migrations.mdx @@ -786,25 +786,25 @@ The following table contains all of the available column modifiers. This list do | Modifier | Description | | -------------------------- | ----------- | -| `.after("column")` | Place the column "after" another column (MySQL). | +| `.after("column")` | Place the column "after" another column (MySQL). | | `.autoIncrement()` | Set INTEGER columns as auto-incrementing (primary key). | -| `.charset("utf8mb4")` | Specify a character set for the column (MySQL). | -| `.collation("utf8mb4_unicode_ci")` | Specify a collation for the column (MySQL/PostgreSQL/SQL Server). | -| `.comment("my comment")` | Add a comment to a column (MySQL/PostgreSQL). | -| `.defaultValue(value)` | Specify a "default" value for the column. | -| `.first()` | Place the column "first" in the table (MySQL). | -| `.from(integer)` | Set the starting value of an auto-incrementing field, an alias for `startingValue()` (MySQL / PostgreSQL). | -| `.invisible()` | Make the column "invisible" to `SELECT *` queries (MySQL). | +| `.charset("utf8mb4")` | Specify a character set for the column (MySQL). | +| `.collation("utf8mb4_unicode_ci")` | Specify a collation for the column (MySQL/PostgreSQL/SQL Server). | +| `.comment("my comment")` | Add a comment to a column (MySQL / PostgreSQL).
Special characters are escaped. | +| `.defaultValue(value)` | Specify a "default" value for the column.
Special characters are escaped. | +| `.first()` | Place the column "first" in the table (MySQL). | +| `.from(integer)` | Set the starting value of an auto-incrementing field, an alias for `startingValue()` (MySQL / PostgreSQL). | +| `.invisible()` | Make the column "invisible" to `SELECT *` queries (MySQL). | | `.nullable(value = true)` | Allow NULL values to be inserted into the column. | -| `.startingValue(integer)` | Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). | -| `.storedAs(expression)` | Create a stored generated column (MySQL / PostgreSQL). | -| `.unsigned()` | Set INTEGER columns as UNSIGNED (MySQL). | +| `.startingValue(integer)` | Set the starting value of an auto-incrementing field (MySQL / PostgreSQL). | +| `.storedAs(expression)` | Create a stored generated column (MySQL / PostgreSQL). | +| `.unsigned()` | Set INTEGER columns as UNSIGNED (MySQL). | | `.useCurrent()` | Set TIMESTAMP columns to use CURRENT_TIMESTAMP as default value. | | `.useCurrentOnUpdate()` | Set TIMESTAMP columns to use CURRENT_TIMESTAMP when a record is updated. | -| `.virtualAs(expression)` | Create a virtual generated column (MySQL). | -| `.generatedAs(expression)` | Create an identity column with specified sequence options (PostgreSQL). | -| `.always()` | Defines the precedence of sequence values over input for an identity column (PostgreSQL). | -| `.isGeometry()` | Set spatial column type to `geometry` - the default type is `geography` (PostgreSQL). | +| `.virtualAs(expression)` | Create a virtual generated column (MySQL). | +| `.generatedAs(expression)` | Create an identity column with specified sequence options (PostgreSQL). | +| `.always()` | Defines the precedence of sequence values over input for an identity column (PostgreSQL). | +| `.isGeometry()` | Set spatial column type to `geometry` - the default type is `geography` (PostgreSQL). | #### Default Expressions diff --git a/docs/supported-compilers.mdx b/docs/supported-compilers.mdx index 69a3973f4..9e45e681d 100644 --- a/docs/supported-compilers.mdx +++ b/docs/supported-compilers.mdx @@ -8,7 +8,7 @@ keywords: [c++ orm, supported compilers, tinyorm] # Supported Compilers -Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then 973 unit tests 😮. +Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then 975 unit tests 😮. Windows >=10: diff --git a/include/orm/schema/grammars/mysqlschemagrammar.hpp b/include/orm/schema/grammars/mysqlschemagrammar.hpp index 0594c165f..89adfe52b 100644 --- a/include/orm/schema/grammars/mysqlschemagrammar.hpp +++ b/include/orm/schema/grammars/mysqlschemagrammar.hpp @@ -162,8 +162,8 @@ namespace Grammars /*! Wrap a single string in keyword identifiers. */ QString wrapValue(QString value) const override; - /*! Escape all MySQL spacial characters desribed in String Literal docs. */ - QString addSlashes(QString value) const; + /*! Escape special characters (used by the defaultValue and comment). */ + QString escapeString(QString value) const override; /*! Get the SQL for the column data type. */ QString getType(const ColumnDefinition &column) const override; diff --git a/include/orm/schema/grammars/postgresschemagrammar.hpp b/include/orm/schema/grammars/postgresschemagrammar.hpp index 8401e161c..ee73c14e3 100644 --- a/include/orm/schema/grammars/postgresschemagrammar.hpp +++ b/include/orm/schema/grammars/postgresschemagrammar.hpp @@ -159,6 +159,9 @@ namespace Grammars QVector compileDropConstraint(const Blueprint &blueprint, const IndexCommand &command) const; + /*! Escape special characters (used by the defaultValue and comment). */ + QString escapeString(QString value) const override; + /*! Get the SQL for the column data type. */ QString getType(const ColumnDefinition &column) const override; diff --git a/include/orm/schema/grammars/schemagrammar.hpp b/include/orm/schema/grammars/schemagrammar.hpp index 1123d9f9f..a922e2b7d 100644 --- a/include/orm/schema/grammars/schemagrammar.hpp +++ b/include/orm/schema/grammars/schemagrammar.hpp @@ -96,6 +96,9 @@ namespace Grammars const Blueprint &blueprint) const = 0; protected: + /*! Escape special characters (used by the defaultValue and comment). */ + virtual QString escapeString(QString value) const = 0; + /*! Get the SQL for the column data type. */ virtual QString getType(const ColumnDefinition &column) const = 0; /*! Compile the blueprint's column definitions. */ diff --git a/include/orm/schema/grammars/sqliteschemagrammar.hpp b/include/orm/schema/grammars/sqliteschemagrammar.hpp index 52e3ac79b..43e789f4b 100644 --- a/include/orm/schema/grammars/sqliteschemagrammar.hpp +++ b/include/orm/schema/grammars/sqliteschemagrammar.hpp @@ -52,6 +52,9 @@ namespace Orm::SchemaNs::Grammars QString addModifiers(QString &&sql, const ColumnDefinition &column) const override; + /*! Escape special characters (used by the defaultValue and comment). */ + QString escapeString(QString value) const override; + /*! Get the SQL for the column data type. */ QString getType(const ColumnDefinition &column) const override; }; diff --git a/src/orm/schema/grammars/mysqlschemagrammar.cpp b/src/orm/schema/grammars/mysqlschemagrammar.cpp index a627cdac7..90d00c501 100644 --- a/src/orm/schema/grammars/mysqlschemagrammar.cpp +++ b/src/orm/schema/grammars/mysqlschemagrammar.cpp @@ -439,18 +439,29 @@ QString MySqlSchemaGrammar::wrapValue(QString value) const QStringLiteral("``"))); } -QString MySqlSchemaGrammar::addSlashes(QString value) const +QString MySqlSchemaGrammar::escapeString(QString value) const { + /* Different approach used for the MySQL and PostgreSQL, for MySQL are escaped more + special characters but for PostrgreSQL only single-quote, it doesn't matter + though, it will work anyway. + On MySQL escaping of ^Z, \0, and \ is needed on some environments, described here: + https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + On PostgreSQL escaping using \ is is more SQL standard conforming, described here, + (especially look at the caution box): + https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-SPECIAL-CHARS*/ + return value - .replace(QChar(0x001a), "^Z") - .replace(QChar('\\'), "\\\\") - .replace(QChar(QChar::Null), "\\0") - .replace(QChar(QChar::LineFeed), "\\n") - .replace(QChar(QChar::Tabulation), "\\t") - .replace(QChar(0x0008), "\\b") - .replace(QChar(QChar::CarriageReturn), "\\r") - .replace(QChar('"'), "\\\"") - .replace(QChar(0x0027), "\\'"); + // No need to escape these +// .replace(QChar(QChar::LineFeed), "\\n") +// .replace(QChar(QChar::Tabulation), "\\t") +// .replace(QChar(0x0008), "\\b") +// .replace(QChar(QChar::CarriageReturn), "\\r") +// .replace(QChar('"'), "\\\"") + .replace(QChar(0x001a), QStringLiteral("^Z")) + .replace(QChar('\\'), QStringLiteral("\\\\")) + .replace(QChar(QChar::Null), QStringLiteral("\\0")) + // 0x0027 = ' + .replace(QChar(0x0027), "''"); } QString MySqlSchemaGrammar::getType(const ColumnDefinition &column) const @@ -902,6 +913,7 @@ QString MySqlSchemaGrammar::modifyDefault(const ColumnDefinition &column) const return {}; // CUR schema, note about security in docs, unprepared and unescaped silverqx + // Default value is already quoted and escaped return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue)); } @@ -940,9 +952,8 @@ QString MySqlSchemaGrammar::modifyComment(const ColumnDefinition &column) const if (column.comment.isEmpty()) return {}; - // CUR schema docs, note about escaping silverqx // All escaped special characters will be correctly saved in the comment - return QStringLiteral(" comment %1").arg(quoteString(addSlashes(column.comment))); + return QStringLiteral(" comment %1").arg(quoteString(escapeString(column.comment))); } QString MySqlSchemaGrammar::modifySrid(const ColumnDefinition &column) const diff --git a/src/orm/schema/grammars/postgresschemagrammar.cpp b/src/orm/schema/grammars/postgresschemagrammar.cpp index 57ea9bd02..53fcdfc54 100644 --- a/src/orm/schema/grammars/postgresschemagrammar.cpp +++ b/src/orm/schema/grammars/postgresschemagrammar.cpp @@ -298,13 +298,9 @@ QVector PostgresSchemaGrammar::compileComment(const Blueprint &blueprint, const CommentCommand &command) const { - auto comment = command.comment; - // Escape single quotes - comment.replace(QChar('\''), QStringLiteral("''")); - return {QStringLiteral("comment on column %1.%2 is %3") .arg(wrapTable(blueprint), BaseGrammar::wrap(command.column), - quoteString(comment))}; + quoteString(escapeString(command.comment)))}; } QVector @@ -442,6 +438,21 @@ PostgresSchemaGrammar::compileDropConstraint(const Blueprint &blueprint, .arg(wrapTable(blueprint), BaseGrammar::wrap(command.index))}; } +QString PostgresSchemaGrammar::escapeString(QString value) const +{ + /* Different approach used for the MySQL and PostgreSQL, for MySQL are escaped more + special characters but for PostrgreSQL only single-quote, it doesn't matter + though, it will work anyway. + On MySQL escaping of ^Z, \0, and \ is needed on some environments, described here: + https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + On PostgreSQL escaping using \ is is more SQL standard conforming, described here, + (especially look at the caution box): + https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-SPECIAL-CHARS*/ + + // 0x0027 = ' + return value.replace(QChar(0x0027), QStringLiteral("''")); +} + QString PostgresSchemaGrammar::getType(const ColumnDefinition &column) const { switch (column.type) { @@ -884,7 +895,7 @@ QString PostgresSchemaGrammar::modifyDefault(const ColumnDefinition &column) con if (!defaultValue.isValid() || defaultValue.isNull()) return {}; - // CUR schema, note about security in docs, unprepared and unescaped silverqx + // Default value is already quoted and escaped return QStringLiteral(" default %1").arg(getDefaultValue(defaultValue)); } diff --git a/src/orm/schema/grammars/schemagrammar.cpp b/src/orm/schema/grammars/schemagrammar.cpp index b33920b95..aa5a2c509 100644 --- a/src/orm/schema/grammars/schemagrammar.cpp +++ b/src/orm/schema/grammars/schemagrammar.cpp @@ -147,7 +147,7 @@ QString SchemaGrammar::getDefaultValue(const QVariant &value) const return value.userType() == QMetaType::Bool #endif ? quoteString(QString::number(value.value())) - : quoteString(value.value()); + : quoteString(escapeString(value.value())); } QString SchemaGrammar::typeComputed(const ColumnDefinition &/*unused*/) const diff --git a/src/orm/schema/grammars/sqliteschemagrammar.cpp b/src/orm/schema/grammars/sqliteschemagrammar.cpp index ff1da0455..9dfdd2466 100644 --- a/src/orm/schema/grammars/sqliteschemagrammar.cpp +++ b/src/orm/schema/grammars/sqliteschemagrammar.cpp @@ -52,6 +52,11 @@ QString SQLiteSchemaGrammar::addModifiers(QString &&/*unused*/, throw Exceptions::RuntimeError(NotImplemented); } +QString SQLiteSchemaGrammar::escapeString(QString /*unused*/) const +{ + throw Exceptions::RuntimeError(NotImplemented); +} + QString SQLiteSchemaGrammar::getType(const ColumnDefinition &/*unused*/) const { throw Exceptions::RuntimeError(NotImplemented); diff --git a/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp b/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp index 0a48b0166..738addbc4 100644 --- a/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp +++ b/tests/auto/unit/orm/schema/mysql_schemabuilder/tst_mysql_schemabuilder.cpp @@ -74,6 +74,7 @@ private Q_SLOTS: void modifiers() const; void modifier_defaultValue_WithExpression() const; void modifier_defaultValue_WithBoolean() const; + void modifier_defaultValue_Escaping() const; void useCurrent() const; void useCurrentOnUpdate() const; @@ -788,6 +789,33 @@ void tst_Mysql_SchemaBuilder::modifier_defaultValue_WithBoolean() const QVERIFY(firstLog.boundValues.isEmpty()); } +void tst_Mysql_SchemaBuilder::modifier_defaultValue_Escaping() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + // String contains \t after tab word + table.string("string").defaultValue(R"(Text ' and " or \ newline +and tab end)"); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + // String contains \t after tab word + "create table `firewalls` (" + "`string` varchar(255) not null default 'Text '' and \" or \\\\ newline\n" + "and tab end') " + "default character set utf8mb4 collate 'utf8mb4_0900_ai_ci' " + "engine = InnoDB"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + void tst_Mysql_SchemaBuilder::useCurrent() const { auto log = DB::connection(m_connection).pretend([](auto &connection) diff --git a/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp b/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp index 257f3ebf0..f25245c7c 100644 --- a/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp +++ b/tests/auto/unit/orm/schema/postgresql_schemabuilder/tst_postgresql_schemabuilder.cpp @@ -74,6 +74,7 @@ private Q_SLOTS: void modifiers() const; void modifier_defaultValue_WithExpression() const; void modifier_defaultValue_WithBoolean() const; + void modifier_defaultValue_Escaping() const; void useCurrent() const; void useCurrentOnUpdate() const; @@ -780,6 +781,32 @@ void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_WithBoolean() const QVERIFY(firstLog.boundValues.isEmpty()); } +void tst_PostgreSQL_SchemaBuilder::modifier_defaultValue_Escaping() const +{ + auto log = DB::connection(m_connection).pretend([](auto &connection) + { + Schema::on(connection.getName()) + .create(Firewalls, [](Blueprint &table) + { + // String contains \t after tab word + table.string("string").defaultValue(R"(Text ' and " or \ newline +and tab end)"); + }); + }); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + // String contains \t after tab word + "create table \"firewalls\" (" + "\"string\" varchar(255) not null " + "default 'Text '' and \" or \\ newline\n" + "and tab end')"); + QVERIFY(firstLog.boundValues.isEmpty()); +} + void tst_PostgreSQL_SchemaBuilder::useCurrent() const { auto log = DB::connection(m_connection).pretend([](auto &connection)