diff --git a/include/PgSQLErrorFields.h b/include/PgSQLErrorFields.h new file mode 100644 index 000000000..05186e580 --- /dev/null +++ b/include/PgSQLErrorFields.h @@ -0,0 +1,39 @@ +/** + * @file PgSQLErrorFields.h + * @brief Parser for PostgreSQL ErrorResponse message fields. + * + * Extracted for unit testability. Scans ErrorResponse payload for + * SQLSTATE ('C') and message ('M') fields. + * + * @see PostgreSQL Protocol: ErrorResponse message format + */ + +#ifndef PGSQL_ERROR_FIELDS_H +#define PGSQL_ERROR_FIELDS_H + +#include +#include + +/** + * @brief Result of parsing a PostgreSQL ErrorResponse payload. + */ +struct PgSQLErrorResult { + bool parsed; ///< True if payload was non-null and scanned. + char sqlstate[6]; ///< 5-char SQLSTATE + null terminator (empty if not found). + const char* message; ///< Pointer into payload at 'M' field value (null if not found). + size_t message_len; ///< Length of message string. +}; + +/** + * @brief Parse a PostgreSQL ErrorResponse payload to extract SQLSTATE and message. + * + * Scans the field-type/value pairs in the ErrorResponse payload. + * Field format: type_byte + null_terminated_string, repeated, ending with '\0'. + * + * @param payload ErrorResponse message payload (after the 5-byte header). + * @param len Length of the payload. + * @return PgSQLErrorResult with parsed fields. + */ +PgSQLErrorResult pgsql_parse_error_response(const unsigned char* payload, size_t len); + +#endif // PGSQL_ERROR_FIELDS_H diff --git a/lib/Makefile b/lib/Makefile index cc7f3bda7..23eb8984b 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -110,6 +110,7 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo TransactionState.oo \ HostgroupRouting.oo \ PgSQLCommandComplete.oo \ + PgSQLErrorFields.oo \ MySQLProtocolUtils.oo \ PgSQLErrorClassifier.oo \ PgSQLMonitorDecision.oo \ diff --git a/lib/PgSQLErrorFields.cpp b/lib/PgSQLErrorFields.cpp new file mode 100644 index 000000000..c18edc0cf --- /dev/null +++ b/lib/PgSQLErrorFields.cpp @@ -0,0 +1,37 @@ +#include "PgSQLErrorFields.h" +#include + +PgSQLErrorResult pgsql_parse_error_response(const unsigned char* payload, size_t len) { + PgSQLErrorResult result {}; + result.parsed = false; + result.sqlstate[0] = '\0'; + result.message = nullptr; + result.message_len = 0; + + if (!payload || len == 0) return result; + result.parsed = true; + + size_t pos = 0; + while (pos < len) { + char field_type = static_cast(payload[pos]); + if (field_type == '\0') break; // end of fields + pos++; // skip field type byte + + // Find the null terminator for this field's value + const char* value = reinterpret_cast(payload + pos); + size_t value_len = strnlen(value, len - pos); + if (pos + value_len >= len) break; // truncated + + if (field_type == 'C' && value_len <= 5) { + memcpy(result.sqlstate, value, value_len); + result.sqlstate[value_len] = '\0'; + } else if (field_type == 'M') { + result.message = value; + result.message_len = value_len; + } + + pos += value_len + 1; // skip value + null terminator + } + + return result; +} diff --git a/test/tap/tests/unit/ffto_protocol_unit-t.cpp b/test/tap/tests/unit/ffto_protocol_unit-t.cpp index fd63eac44..dfacc5c77 100644 --- a/test/tap/tests/unit/ffto_protocol_unit-t.cpp +++ b/test/tap/tests/unit/ffto_protocol_unit-t.cpp @@ -16,6 +16,7 @@ #include "proxysql.h" #include "MySQLProtocolUtils.h" #include "PgSQLCommandComplete.h" +#include "PgSQLErrorFields.h" #include #include @@ -309,12 +310,56 @@ static void test_mysql_err_packet_empty_message() { ok(errmsg_len == 0, "ERR parse empty msg: empty message"); } +// ============================================================================ +// 7. PgSQL: ErrorResponse field parsing +// ============================================================================ + +static void test_pgsql_error_fields_basic() { + unsigned char payload[] = { + 'S','E','R','R','O','R','\0', + 'C','4','2','6','0','1','\0', + 'M','s','y','n','t','a','x',' ','e','r','r','o','r','\0', + '\0' + }; + PgSQLErrorResult r = pgsql_parse_error_response(payload, sizeof(payload)); + ok(r.parsed == true, "PgSQL error parse: basic parsed"); + ok(strcmp(r.sqlstate, "42601") == 0, "PgSQL error parse: sqlstate = 42601"); + ok(r.message != nullptr && strncmp(r.message, "syntax error", 12) == 0, + "PgSQL error parse: message starts with 'syntax error'"); +} + +static void test_pgsql_error_fields_missing_code() { + unsigned char payload[] = { + 'S','E','R','R','O','R','\0', + 'M','s','o','m','e',' ','e','r','r','\0', + '\0' + }; + PgSQLErrorResult r = pgsql_parse_error_response(payload, sizeof(payload)); + ok(r.parsed == true, "PgSQL error no-code: parsed"); + ok(strlen(r.sqlstate) == 0, "PgSQL error no-code: empty sqlstate"); + ok(r.message != nullptr && strncmp(r.message, "some err", 8) == 0, + "PgSQL error no-code: message correct"); +} + +static void test_pgsql_error_fields_empty() { + unsigned char payload[] = {'\0'}; + PgSQLErrorResult r = pgsql_parse_error_response(payload, 1); + ok(r.parsed == true, "PgSQL error empty: parsed (no fields)"); + ok(strlen(r.sqlstate) == 0, "PgSQL error empty: empty sqlstate"); + ok(r.message == nullptr, "PgSQL error empty: null message"); +} + +static void test_pgsql_error_fields_zero_length() { + PgSQLErrorResult r = pgsql_parse_error_response(nullptr, 0); + ok(r.parsed == false, "PgSQL error null: returns false"); +} + // ============================================================================ // Main // ============================================================================ int main() { - plan(46); + plan(56); int rc = test_init_minimal(); ok(rc == 0, "test_init_minimal() succeeds"); @@ -355,6 +400,12 @@ int main() { test_mysql_err_packet_truncated(); // 1 test_mysql_err_packet_empty_message(); // 3 + // PgSQL ErrorResponse parsing + test_pgsql_error_fields_basic(); // 3 + test_pgsql_error_fields_missing_code(); // 3 + test_pgsql_error_fields_empty(); // 3 + test_pgsql_error_fields_zero_length(); // 1 + test_cleanup_minimal(); return exit_status(); }