You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/test/tap/tests/unit/ffto_protocol_unit-t.cpp

292 lines
10 KiB

/**
* @file ffto_protocol_unit-t.cpp
* @brief Comprehensive unit tests for FFTO protocol parsing utilities.
*
* Tests both MySQL and PgSQL protocol parsing functions used by FFTO:
* - MySQL: read_lenenc_int, packet building, OK packet parsing
* - PgSQL: CommandComplete tag parsing
* - Both: fragmented data reassembly simulation, large payloads
*
* @see FFTO unit testing (GitHub issue #5499)
*/
#include "tap.h"
#include "test_globals.h"
#include "test_init.h"
#include "proxysql.h"
#include "MySQLProtocolUtils.h"
#include "PgSQLCommandComplete.h"
#include <cstring>
#include <vector>
// ============================================================================
// 1. MySQL: read_lenenc_int
// ============================================================================
static void test_mysql_lenenc_1byte() {
unsigned char buf[] = {0};
const unsigned char *p = buf; size_t len = 1;
ok(mysql_read_lenenc_int(p, len) == 0, "lenenc: 0x00 → 0");
ok(len == 0 && p == buf + 1, "lenenc: consumed 1 byte");
unsigned char buf2[] = {250};
p = buf2; len = 1;
ok(mysql_read_lenenc_int(p, len) == 250, "lenenc: 0xFA → 250 (max 1-byte)");
}
static void test_mysql_lenenc_2byte() {
unsigned char buf[] = {0xFC, 0x01, 0x00};
const unsigned char *p = buf; size_t len = 3;
ok(mysql_read_lenenc_int(p, len) == 1, "lenenc 2-byte: 0xFC 01 00 → 1");
ok(len == 0, "lenenc 2-byte: consumed 3 bytes");
unsigned char buf2[] = {0xFC, 0xFF, 0xFF};
p = buf2; len = 3;
ok(mysql_read_lenenc_int(p, len) == 65535, "lenenc 2-byte: max → 65535");
}
static void test_mysql_lenenc_3byte() {
unsigned char buf[] = {0xFD, 0x00, 0x00, 0x01, 0x00}; // padded for CPY safety
const unsigned char *p = buf; size_t len = 4;
ok(mysql_read_lenenc_int(p, len) == 65536, "lenenc 3-byte: → 65536");
}
static void test_mysql_lenenc_8byte() {
unsigned char buf[9] = {0xFE, 0x01, 0, 0, 0, 0, 0, 0, 0};
const unsigned char *p = buf; size_t len = 9;
ok(mysql_read_lenenc_int(p, len) == 1, "lenenc 8-byte: → 1");
unsigned char buf2[9] = {0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0};
p = buf2; len = 9;
ok(mysql_read_lenenc_int(p, len) == 0xFFFFFFFF, "lenenc 8-byte: → 4294967295");
}
static void test_mysql_lenenc_truncated() {
// 2-byte prefix but only 1 byte of data
unsigned char buf[] = {0xFC, 0x01};
const unsigned char *p = buf; size_t len = 2;
ok(mysql_read_lenenc_int(p, len) == 0, "lenenc truncated: 0xFC with 1 byte → 0");
// Empty buffer
const unsigned char *p2 = nullptr; size_t len2 = 0;
ok(mysql_read_lenenc_int(p2, len2) == 0, "lenenc empty: → 0");
}
// ============================================================================
// 2. MySQL: packet building
// ============================================================================
static void test_mysql_build_packet() {
unsigned char payload[] = {0x03, 'S', 'E', 'L', 'E', 'C', 'T', ' ', '1'};
unsigned char out[13];
size_t total = mysql_build_packet(payload, 9, 0, out);
ok(total == 13, "build packet: total size 13");
ok(out[0] == 9 && out[1] == 0 && out[2] == 0, "build packet: length = 9");
ok(out[3] == 0, "build packet: seq_id = 0");
ok(memcmp(out + 4, payload, 9) == 0, "build packet: payload intact");
}
static void test_mysql_build_large_packet() {
// Build a packet with 1000-byte payload
std::vector<unsigned char> payload(1000, 'X');
std::vector<unsigned char> out(1004);
size_t total = mysql_build_packet(payload.data(), 1000, 5, out.data());
ok(total == 1004, "large packet: total size 1004");
ok(out[0] == 0xE8 && out[1] == 0x03 && out[2] == 0x00,
"large packet: length = 1000 (little-endian)");
ok(out[3] == 5, "large packet: seq_id = 5");
}
static void test_mysql_build_empty_packet() {
unsigned char out[4];
size_t total = mysql_build_packet(nullptr, 0, 1, out);
ok(total == 4, "empty packet: header only");
ok(out[0] == 0 && out[1] == 0 && out[2] == 0, "empty packet: length = 0");
}
// ============================================================================
// 3. MySQL: OK packet affected_rows extraction
// ============================================================================
static void test_mysql_ok_affected_rows() {
// Build an OK packet: 0x00 + affected_rows(lenenc) + last_insert_id(lenenc)
// affected_rows = 42
unsigned char ok_payload[] = {0x00, 42, 0}; // OK, affected=42, last_insert=0
unsigned char pkt[7];
mysql_build_packet(ok_payload, 3, 1, pkt);
// Parse affected_rows from the OK packet payload
const unsigned char *pos = ok_payload + 1;
size_t rem = 2;
uint64_t affected = mysql_read_lenenc_int(pos, rem);
ok(affected == 42, "OK packet: affected_rows = 42");
}
static void test_mysql_ok_large_affected_rows() {
// affected_rows = 300 (needs 0xFC prefix)
unsigned char ok_payload[] = {0x00, 0xFC, 0x2C, 0x01, 0};
const unsigned char *pos = ok_payload + 1;
size_t rem = 4;
uint64_t affected = mysql_read_lenenc_int(pos, rem);
ok(affected == 300, "OK packet: affected_rows = 300 (2-byte lenenc)");
}
// ============================================================================
// 4. PgSQL: CommandComplete — extended tests
// ============================================================================
static void test_pgsql_insert_with_oid() {
// "INSERT oid count" format — the count is always the last token
auto r = parse_pgsql_command_complete(
(const unsigned char *)"INSERT 12345 50", 15);
ok(r.rows == 50 && r.is_select == false,
"PgSQL INSERT with OID: rows=50 (last token)");
}
static void test_pgsql_large_row_count() {
auto r = parse_pgsql_command_complete(
(const unsigned char *)"SELECT 1000000", 14);
ok(r.rows == 1000000 && r.is_select == true,
"PgSQL large SELECT: rows=1000000");
}
static void test_pgsql_zero_rows() {
auto r = parse_pgsql_command_complete(
(const unsigned char *)"UPDATE 0", 8);
ok(r.rows == 0 && r.is_select == false, "PgSQL UPDATE 0: rows=0");
auto r2 = parse_pgsql_command_complete(
(const unsigned char *)"SELECT 0", 8);
ok(r2.rows == 0 && r2.is_select == true, "PgSQL SELECT 0: rows=0");
}
static void test_pgsql_all_command_types() {
struct { const char *tag; uint64_t expected; bool is_sel; } cases[] = {
{"INSERT 0 1", 1, false},
{"UPDATE 5", 5, false},
{"DELETE 3", 3, false},
{"SELECT 10", 10, true},
{"FETCH 7", 7, true},
{"MOVE 2", 2, true},
{"COPY 100", 100, false},
{"MERGE 8", 8, false},
};
int pass = 0;
for (auto &c : cases) {
auto r = parse_pgsql_command_complete(
(const unsigned char *)c.tag, strlen(c.tag));
if (r.rows == c.expected && r.is_select == c.is_sel) pass++;
}
ok(pass == 8, "PgSQL all 8 command types parse correctly");
}
static void test_pgsql_ddl_commands() {
const char *ddls[] = {
"CREATE TABLE", "ALTER TABLE", "DROP TABLE",
"CREATE INDEX", "DROP INDEX", "VACUUM",
"TRUNCATE TABLE", "GRANT", "REVOKE",
};
int pass = 0;
for (auto &ddl : ddls) {
auto r = parse_pgsql_command_complete(
(const unsigned char *)ddl, strlen(ddl));
if (r.rows == 0) pass++;
}
ok(pass == 9, "PgSQL DDL commands all return rows=0");
}
static void test_pgsql_null_terminated_payload() {
// Payload with null terminator (common in real wire format)
unsigned char payload[] = {'S', 'E', 'L', 'E', 'C', 'T', ' ', '5', '\0'};
auto r = parse_pgsql_command_complete(payload, 9);
ok(r.rows == 5, "PgSQL null-terminated: SELECT 5 → rows=5");
}
// ============================================================================
// 5. Fragmented data simulation
// ============================================================================
static void test_mysql_fragmented_lenenc() {
// Simulate reading a lenenc int where data arrives in chunks
// Build a 3-byte lenenc (0xFD prefix + 3 bytes)
unsigned char full[] = {0xFD, 0x40, 0x42, 0x0F}; // = 999,999 + 1 (not quite, but valid)
// First chunk: just the prefix
const unsigned char *p = full; size_t len = 1;
uint64_t val = mysql_read_lenenc_int(p, len);
// With only 1 byte, the 0xFD prefix is consumed but there aren't 3 bytes after → returns 0
ok(val == 0, "fragmented: 0xFD with no data bytes → 0 (truncated)");
// Full data
p = full; len = 4;
val = mysql_read_lenenc_int(p, len);
ok(val > 0, "fragmented: full 3-byte lenenc decoded successfully");
}
static void test_mysql_multi_packet_build() {
// Build 3 packets sequentially (simulating a multi-packet stream)
unsigned char stream[64];
size_t offset = 0;
unsigned char p1[] = {0x03, 'S', 'E', 'L'};
offset += mysql_build_packet(p1, 4, 0, stream + offset);
unsigned char p2[] = {0x01, 0x02, 0x03};
offset += mysql_build_packet(p2, 3, 1, stream + offset);
unsigned char p3[] = {0xFE, 0x00, 0x00, 0x00, 0x00};
offset += mysql_build_packet(p3, 5, 2, stream + offset);
ok(offset == 4+4 + 3+4 + 5+4, "multi-packet: total stream size correct");
// Verify each packet header
ok(stream[0] == 4 && stream[3] == 0, "multi-packet: pkt 0 header ok");
ok(stream[8] == 3 && stream[11] == 1, "multi-packet: pkt 1 header ok");
ok(stream[15] == 5 && stream[18] == 2, "multi-packet: pkt 2 header ok");
}
// ============================================================================
// Main
// ============================================================================
int main() {
plan(36);
int rc = test_init_minimal();
ok(rc == 0, "test_init_minimal() succeeds");
// MySQL lenenc
test_mysql_lenenc_1byte(); // 3
test_mysql_lenenc_2byte(); // 3
test_mysql_lenenc_3byte(); // 1
test_mysql_lenenc_8byte(); // 2
test_mysql_lenenc_truncated(); // 2
// MySQL packet building
test_mysql_build_packet(); // 4
test_mysql_build_large_packet(); // 3
test_mysql_build_empty_packet(); // 2
// MySQL OK packet parsing
test_mysql_ok_affected_rows(); // 1
test_mysql_ok_large_affected_rows(); // 1
// PgSQL extended
test_pgsql_insert_with_oid(); // 1
test_pgsql_large_row_count(); // 1
test_pgsql_zero_rows(); // 2
test_pgsql_all_command_types(); // 1
test_pgsql_ddl_commands(); // 1
test_pgsql_null_terminated_payload(); // 1
// Fragmentation
test_mysql_fragmented_lenenc(); // 2
test_mysql_multi_packet_build(); // 4
// Total: 1+3+3+1+2+2+4+3+2+1+1+1+1+2+1+1+1+2+4 = 36... recount
// 1 (init) + 3+3+1+2+2 (lenenc=11) + 4+3+2 (build=9) + 1+1 (ok=2) +
// 1+1+2+1+1+1 (pgsql=7) + 2+4 (frag=6) = 1+11+9+2+7+6 = 36
test_cleanup_minimal();
return exit_status();
}