diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index 064eb52cc..79d82d1f1 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -310,6 +310,151 @@ const std::vector LOAD_COREDUMP_FROM_MEMORY = { extern unordered_map, vector>> load_save_disk_commands; +// Helper function: Escape single quotes in a string for SQL safety +// Returns number of chars written to dst (excluding null terminator) +static size_t escape_sql_string(char* dst, const char* src, size_t dst_size) { + if (!dst || !src || dst_size == 0) return 0; + + size_t i = 0, j = 0; + while (src[i] && j < dst_size - 1) { + if (src[i] == '\'') { + // Escape single quote by doubling it + if (j < dst_size - 2) { + dst[j++] = '\''; + dst[j++] = '\''; + } + } + else { + dst[j++] = src[i]; + } + i++; + } + dst[j] = '\0'; + return j; +} + +// Helper function: Extract pattern from c.relname OPERATOR or LIKE clause +// Returns true if pattern was found, false otherwise +// pattern_buf must be at least 128 bytes +static bool extract_psql_pattern(const char* query, char* pattern_buf, size_t buf_size) { + if (!query || !pattern_buf || buf_size < 64) return false; + + pattern_buf[0] = '\0'; + + // Look for pattern in c.relname + char* relname_pos = strcasestr((char*)query, "c.relname"); + if (!relname_pos) return false; + + // Skip to the operator or LIKE keyword + char* value_pos = relname_pos + strlen("c.relname"); + while (*value_pos && *value_pos == ' ') value_pos++; + + // Safety check: ensure we haven't gone past end of string + if (!*value_pos) return false; + + char* pattern = nullptr; + + // Check for LIKE operator first + char* like_pos = strcasestr(value_pos, "LIKE"); + if (like_pos) { + like_pos += 4; // Skip "LIKE" + while (*like_pos && *like_pos == ' ') like_pos++; + pattern = like_pos; + } + // Check for OPERATOR operator + else if (strcasestr(value_pos, "OPERATOR")) { + char* op_pos = strcasestr(value_pos, "OPERATOR"); + char* value_start = strchr(op_pos, '\''); + if (value_start) { + value_start++; + pattern = value_start; + } + } + + if (!pattern) return false; + + // Skip leading spaces + while (*pattern && *pattern == ' ') pattern++; + + // Find end of pattern (first quote or newline) + char* end = pattern; + size_t max_len = buf_size / 2 - 1; // Leave room for escaping + while (*end && *end != '\'' && *end != '\n' && (end - pattern) < (int)max_len) end++; + + // Copy and escape to safe buffer + size_t len = end - pattern; + if (len > 0) { + escape_sql_string(pattern_buf, pattern, buf_size); + return true; + } + + return false; +} + +// Helper function: Convert PostgreSQL regex pattern to SQLite LIKE pattern +// Handles: ^, $, (), and .* to % conversion +// dst_size should be at least 128 (twice src size for escaping) +static void convert_regex_to_like(char* dst, const char* src, size_t dst_size) { + if (!dst || !src || dst_size < 64) return; + + char* dst_end = dst + dst_size - 1; + + // Skip ^ at start + if (*src == '^') src++; + + // Copy pattern, converting regex to LIKE + while (*src && dst < dst_end - 1) { // Reserve space for potential escape + if (*src == '.' && *(src + 1) == '*') { + *dst++ = '%'; + src += 2; + } + else if (*src == '$' && (*(src + 1) == '\0' || *(src + 1) == '\'')) { + // Skip $ at end + break; + } + else if (*src == '(' || *src == ')') { + // Skip regex grouping parentheses + src++; + } + else { + // Escape single quotes for SQL safety + if (*src == '\'' && dst < dst_end - 1) { + *dst++ = '\''; + } + *dst++ = *src++; + } + } + *dst = '\0'; +} + +// Unified handler for \dt, \di, \dv commands +// relkind_char: 'r' for tables, 'i' for indexes, 'v' for views +// sqlite_type: "table", "index", or "view" +// columns: columns to SELECT (e.g., "name" or "name, tbl_name") +static void handle_psql_list_command(const char* query_no_space, char** query, unsigned int* query_length, + char relkind_char, const char* sqlite_type, const char* columns) { + // Check for pattern in WHERE clause + const char* where_clause = strcasestr(query_no_space, "WHERE"); + char pattern_buf[128] = { 0 }; + char converted_pattern[128] = { 0 }; + char buf[512] = { 0 }; + + if (where_clause && extract_psql_pattern(where_clause, pattern_buf, sizeof(pattern_buf))) { + convert_regex_to_like(converted_pattern, pattern_buf, sizeof(converted_pattern)); + // Build query with pattern - note: converted_pattern is already escaped + snprintf(buf, sizeof(buf), "SELECT %s FROM sqlite_master WHERE type='%s' AND name NOT LIKE 'sqlite_%%' AND name LIKE '%s' ORDER BY name", + columns, sqlite_type, converted_pattern); + } + else { + // Build query without pattern + snprintf(buf, sizeof(buf), "SELECT %s FROM sqlite_master WHERE type='%s' AND name NOT LIKE 'sqlite_%%' ORDER BY name", + columns, sqlite_type); + } + + *query = l_strdup(buf); + *query_length = strlen(*query) + 1; +} + bool is_admin_command_or_alias(const std::vector& cmds, char *query_no_space, int query_no_space_length) { for (std::vector::const_iterator it=cmds.begin(); it!=cmds.end(); ++it) { if ((unsigned int)query_no_space_length==it->length() && !strncasecmp(it->c_str(), query_no_space, query_no_space_length)) { @@ -4106,6 +4251,67 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) { goto __run_query; } + + // Handle PostgreSQL meta commands expanded by psql client + // These commands are intercepted and converted to appropriate SQLite queries + if constexpr (std::is_same_v) { + if (query_no_space_length >= strlen("SELECT") && !strncasecmp("SELECT", query_no_space, strlen("SELECT"))) { + // \l, \l+ : List databases + // psql: SELECT ... FROM pg_catalog.pg_database ... + // sqlite: PRAGMA DATABASE_LIST + if (strcasestr(query_no_space, "FROM pg_catalog.pg_database") != nullptr || + strcasestr(query_no_space, "FROM pg_database") != nullptr) { + l_free(query_length, query); + query = l_strdup("PRAGMA DATABASE_LIST"); + query_length = strlen(query) + 1; + goto __run_query; + } + + // \d : List all relations (without table name) + // psql: SELECT ... FROM pg_catalog.pg_class c ... WHERE c.relkind IN ('r','p','v','m','S','f','') + // sqlite: SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index', 'trigger') ... + // Note: \d includes 'v' (views) in relkind, \dt does not + if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr || + strcasestr(query_no_space, "FROM pg_class c") != nullptr) && + strcasestr(query_no_space, "c.relkind IN ('r','p','v'") != nullptr) { + + l_free(query_length, query); + // List all relations + query = l_strdup("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index', 'trigger') AND name NOT LIKE 'sqlite_%' ORDER BY type, name"); + query_length = strlen(query) + 1; + goto __run_query; + } + + // \dt [pattern] : List tables (with optional pattern) + // psql: SELECT ... FROM pg_catalog.pg_class c ... WHERE c.relkind IN ('r','p', ...) + if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr || + strcasestr(query_no_space, "FROM pg_class c") != nullptr) && + strcasestr(query_no_space, "c.relkind IN ('r'") != nullptr) { + l_free(query_length, query); + handle_psql_list_command(query_no_space, &query, &query_length, 'r', "table", "name"); + goto __run_query; + } + + // \di [pattern] : List indexes (with optional pattern) + if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr || + strcasestr(query_no_space, "FROM pg_class c") != nullptr) && + strcasestr(query_no_space, "c.relkind IN ('i'") != nullptr) { + l_free(query_length, query); + handle_psql_list_command(query_no_space, &query, &query_length, 'i', "index", "name, tbl_name"); + goto __run_query; + } + + // \dv [pattern] : List views (with optional pattern) + if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr || + strcasestr(query_no_space, "FROM pg_class c") != nullptr) && + strcasestr(query_no_space, "c.relkind IN ('v'") != nullptr) { + l_free(query_length, query); + handle_psql_list_command(query_no_space, &query, &query_length, 'v', "view", "name"); + goto __run_query; + } + } + } + if (strncasecmp("SHOW ", query_no_space, 5)) { goto __end_show_commands; // in the next block there are only SHOW commands }