diff options
author | Andres Freund <andres@anarazel.de> | 2025-02-10 10:03:38 -0500 |
---|---|---|
committer | Andres Freund <andres@anarazel.de> | 2025-02-10 10:03:38 -0500 |
commit | 02d4d87ac20e2698b5375b347c451c55045e388d (patch) | |
tree | 24669feb94aadf4795d9ae5f024f53ca985d5bfe | |
parent | 43a77239d412db194a69b18b7850580e3b78218f (diff) | |
download | postgresql-02d4d87ac20e2698b5375b347c451c55045e388d.tar.gz postgresql-02d4d87ac20e2698b5375b347c451c55045e388d.zip |
Add test of various escape functions
As highlighted by the prior commit, writing correct escape functions is less
trivial than one might hope.
This test module tries to verify that different escaping functions behave
reasonably. It e.g. tests:
- Invalidly encoded input to an escape function leads to invalidly encoded
output
- Trailing incomplete multi-byte characters are handled sensibly
- Escaped strings are parsed as single statement by psql's parser (which
derives from the backend parser)
There are further tests that would be good to add. But even in the current
state it was rather useful for writing the fix in the prior commit.
Reviewed-by: Noah Misch <noah@leadboat.com>
Backpatch-through: 13
Security: CVE-2025-1094
-rw-r--r-- | src/test/modules/Makefile | 1 | ||||
-rw-r--r-- | src/test/modules/meson.build | 1 | ||||
-rw-r--r-- | src/test/modules/test_escape/.gitignore | 2 | ||||
-rw-r--r-- | src/test/modules/test_escape/Makefile | 27 | ||||
-rw-r--r-- | src/test/modules/test_escape/meson.build | 31 | ||||
-rw-r--r-- | src/test/modules/test_escape/t/001_test_escape.pl | 53 | ||||
-rw-r--r-- | src/test/modules/test_escape/test_escape.c | 803 | ||||
-rw-r--r-- | src/tools/pgindent/typedefs.list | 3 |
8 files changed, 921 insertions, 0 deletions
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 256799f520a..9651f9e5e18 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -19,6 +19,7 @@ SUBDIRS = \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ + test_escape \ test_extensions \ test_ginpostinglist \ test_integerset \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index d8fe059d236..1730d1ac1c4 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -18,6 +18,7 @@ subdir('test_custom_rmgrs') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') +subdir('test_escape') subdir('test_extensions') subdir('test_ginpostinglist') subdir('test_integerset') diff --git a/src/test/modules/test_escape/.gitignore b/src/test/modules/test_escape/.gitignore new file mode 100644 index 00000000000..e498d6b7efa --- /dev/null +++ b/src/test/modules/test_escape/.gitignore @@ -0,0 +1,2 @@ +/tmp_check/ +/test_escape diff --git a/src/test/modules/test_escape/Makefile b/src/test/modules/test_escape/Makefile new file mode 100644 index 00000000000..786db4cbae4 --- /dev/null +++ b/src/test/modules/test_escape/Makefile @@ -0,0 +1,27 @@ +# src/test/modules/test_escape/Makefile + +PGFILEDESC = "test escape program" +PGAPPICON = win32 + +PROGRAM = test_escape +OBJS = $(WIN32RES) test_escape.o + +PG_CPPFLAGS = -I$(libpq_srcdir) +PG_LIBS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) + +NO_INSTALL = 1 +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_escape +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +test_escape$(X): | submake-libpgfeutils +check: test_escape$(X) diff --git a/src/test/modules/test_escape/meson.build b/src/test/modules/test_escape/meson.build new file mode 100644 index 00000000000..a21341d5067 --- /dev/null +++ b/src/test/modules/test_escape/meson.build @@ -0,0 +1,31 @@ +test_escape_sources = files( + 'test_escape.c', +) + +if host_system == 'windows' + test_escape_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_escape', + '--FILEDESC', 'test escape program',]) +endif + +test_escape = executable('test_escape', + test_escape_sources, + dependencies: [frontend_code, libpq], + kwargs: default_bin_args + { + 'install': false, + } +) +testprep_targets += test_escape + + +tests += { + 'name': 'test_escape', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_test_escape.pl', + ], + 'deps': [test_escape], + }, +} diff --git a/src/test/modules/test_escape/t/001_test_escape.pl b/src/test/modules/test_escape/t/001_test_escape.pl new file mode 100644 index 00000000000..0d5aec3ed74 --- /dev/null +++ b/src/test/modules/test_escape/t/001_test_escape.pl @@ -0,0 +1,53 @@ +# Copyright (c) 2023-2025, PostgreSQL Global Development Group +use strict; +use warnings FATAL => 'all'; +use Config; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); + +$node->init(); +$node->start(); + +$node->safe_psql('postgres', + q(CREATE DATABASE db_sql_ascii ENCODING "sql_ascii" TEMPLATE template0;)); + +my $cmd = + [ 'test_escape', '--conninfo', $node->connstr . " dbname=db_sql_ascii" ]; + +# There currently is no good other way to transport test results from a C +# program that requires just the node being set-up... +my ($stderr, $stdout); +my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr; + +is($result, 1, "test_escape returns 0"); +is($stderr, '', "test_escape stderr is empty"); + +foreach my $line (split('\n', $stdout)) +{ + if ($line =~ m/^ok \d+ ?(.*)/) + { + ok(1, $1); + } + + elsif ($line =~ m/^not ok \d+ ?(.*)/) + { + ok(0, $1); + } + + elsif ($line =~ m/^# ?(.*)/) + { + note $1; + } + elsif ($line =~ m/^\d+..\d+$/) + { + } + else + { + BAIL_OUT("no unmapped lines, got $line"); + } +} + +done_testing(); diff --git a/src/test/modules/test_escape/test_escape.c b/src/test/modules/test_escape/test_escape.c new file mode 100644 index 00000000000..6654ab1dbe7 --- /dev/null +++ b/src/test/modules/test_escape/test_escape.c @@ -0,0 +1,803 @@ +/* + * test_escape.c Test escape functions + * + * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_escape/test_escape.c + */ + +#include "postgres_fe.h" + +#include <string.h> +#include <stdio.h> + +#include "fe_utils/psqlscan.h" +#include "fe_utils/string_utils.h" +#include "getopt_long.h" +#include "libpq-fe.h" +#include "mb/pg_wchar.h" + + +typedef struct pe_test_config +{ + int verbosity; + bool force_unsupported; + const char *conninfo; + PGconn *conn; + + int test_count; + int failure_count; +} pe_test_config; + + +/* + * An escape function to be tested by this test. + */ +typedef struct pe_test_escape_func +{ + const char *name; + + /* + * Can the escape method report errors? If so, we validate that it does in + * case of various invalid inputs. + */ + bool reports_errors; + + /* + * Is the escape method known to not handle invalidly encoded input? If + * so, we don't run the test unless --force-unsupported is used. + */ + bool supports_only_valid; + + /* + * Is the escape method known to only handle encodings where no byte in a + * multi-byte characters are valid ascii. + */ + bool supports_only_ascii_overlap; + + bool (*escape) (PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err); +} pe_test_escape_func; + +/* + * A single test input for this test. + */ +typedef struct pe_test_vector +{ + const char *client_encoding; + size_t escape_len; + const char *escape; +} pe_test_vector; + + +/* + * Callback functions from flex lexer. Not currently used by the test. + */ +static const PsqlScanCallbacks test_scan_callbacks = { + NULL +}; + + +static bool +escape_literal(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + char *escaped; + + escaped = PQescapeLiteral(conn, unescaped, unescaped_len); + if (!escaped) + { + appendPQExpBuffer(escape_err, "%s", + PQerrorMessage(conn)); + escape_err->data[escape_err->len - 1] = 0; + escape_err->len--; + return false; + } + else + { + appendPQExpBufferStr(target, escaped); + PQfreemem(escaped); + return true; + } +} + +static bool +escape_identifier(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + char *escaped; + + escaped = PQescapeIdentifier(conn, unescaped, unescaped_len); + if (!escaped) + { + appendPQExpBuffer(escape_err, "%s", + PQerrorMessage(conn)); + escape_err->data[escape_err->len - 1] = 0; + escape_err->len--; + return false; + } + else + { + appendPQExpBufferStr(target, escaped); + PQfreemem(escaped); + return true; + } +} + +static bool +escape_string_conn(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + int error; + size_t sz; + + appendPQExpBufferChar(target, '\''); + enlargePQExpBuffer(target, unescaped_len * 2 + 1); + sz = PQescapeStringConn(conn, target->data + target->len, + unescaped, unescaped_len, + &error); + + target->len += sz; + appendPQExpBufferChar(target, '\''); + + if (error) + { + appendPQExpBuffer(escape_err, "%s", + PQerrorMessage(conn)); + escape_err->data[escape_err->len - 1] = 0; + escape_err->len--; + return false; + } + else + { + return true; + } +} + +static bool +escape_string(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + size_t sz; + + appendPQExpBufferChar(target, '\''); + enlargePQExpBuffer(target, unescaped_len * 2 + 1); + sz = PQescapeString(target->data + target->len, + unescaped, unescaped_len); + target->len += sz; + appendPQExpBufferChar(target, '\''); + + + return true; +} + +/* + * Escape via s/'/''/. Non-core drivers invariably wrap libpq or use this + * method. It suffices iff the input passes encoding validation, so it's + * marked as supports_only_valid. + */ +static bool +escape_replace(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + const char *s = unescaped; + + appendPQExpBufferChar(target, '\''); + + for (int i = 0; i < unescaped_len; i++) + { + char c = *s; + + if (c == '\'') + { + appendPQExpBufferStr(target, "''"); + } + else + appendPQExpBufferChar(target, c); + s++; + } + appendPQExpBufferChar(target, '\''); + + return true; +} + +static bool +escape_append_literal(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + appendStringLiteral(target, unescaped, PQclientEncoding(conn), 1); + + return true; +} + +static bool +escape_fmt_id(PGconn *conn, PQExpBuffer target, + const char *unescaped, size_t unescaped_len, + PQExpBuffer escape_err) +{ + setFmtEncoding(PQclientEncoding(conn)); + appendPQExpBufferStr(target, fmtId(unescaped)); + + return true; +} + +static pe_test_escape_func pe_test_escape_funcs[] = +{ + { + .name = "PQescapeLiteral", + .reports_errors = true, + .escape = escape_literal, + }, + { + .name = "PQescapeIdentifier", + .reports_errors = true, + .escape = escape_identifier + }, + { + .name = "PQescapeStringConn", + .reports_errors = true, + .escape = escape_string_conn + }, + { + .name = "PQescapeString", + .reports_errors = false, + .escape = escape_string + }, + { + .name = "replace", + .reports_errors = false, + .supports_only_valid = true, + .supports_only_ascii_overlap = true, + .escape = escape_replace + }, + { + .name = "appendStringLiteral", + .reports_errors = false, + .escape = escape_append_literal + }, + { + .name = "fmtId", + .reports_errors = false, + .escape = escape_fmt_id + }, +}; + + +#define TV(enc, string) {.client_encoding = (enc), .escape=string, .escape_len=sizeof(string) - 1, } +static pe_test_vector pe_test_vectors[] = +{ + /* expected to work sanity checks */ + TV("UTF-8", "1"), + TV("UTF-8", "'"), + TV("UTF-8", "\""), + + TV("UTF-8", "\'"), + TV("UTF-8", "\""), + + TV("UTF-8", "\\"), + + TV("UTF-8", "\\'"), + TV("UTF-8", "\\\""), + + /* trailing multi-byte character, paddable in available space */ + TV("UTF-8", "1\xC0"), + TV("UTF-8", "1\xE0 "), + TV("UTF-8", "1\xF0 "), + TV("UTF-8", "1\xF0 "), + TV("UTF-8", "1\xF0 "), + + /* trailing multi-byte character, not enough space to pad */ + TV("UTF-8", "1\xE0"), + TV("UTF-8", "1\xF0"), + TV("UTF-8", "\xF0"), + + /* try to smuggle in something in invalid characters */ + TV("UTF-8", "1\xE0'"), + TV("UTF-8", "1\xE0\""), + TV("UTF-8", "1\xF0'"), + TV("UTF-8", "1\xF0\""), + TV("UTF-8", "1\xF0'; "), + TV("UTF-8", "1\xF0\"; "), + TV("UTF-8", "1\xF0';;;;"), + TV("UTF-8", "1\xF0 ';;;;"), + TV("UTF-8", "1\xF0 \";;;;"), + TV("UTF-8", "1\xE0'; \\l ; "), + TV("UTF-8", "1\xE0\"; \\l ; "), + + /* null byte handling */ + TV("UTF-8", "some\0thing"), + TV("UTF-8", "some\0"), + TV("UTF-8", "some\xF0'\0"), + TV("UTF-8", "some\xF0'\0'"), + TV("UTF-8", "some\xF0" "ab\0'"), + + /* GB18030's 4 byte encoding requires a 2nd byte limited values */ + TV("GB18030", "\x90\x31"), + TV("GB18030", "\\\x81\x5c'"), + TV("GB18030", "\\\x81\x5c\""), + TV("GB18030", "\\\x81\x5c\0'"), + + /* + * \x81 indicates a 2 byte char. ' and " are not a valid second byte, but + * that requires encoding verification to know. E.g. replace_string() + * doesn't cope. + */ + TV("GB18030", "\\\x81';"), + TV("GB18030", "\\\x81\";"), + + /* + * \x81 indicates a 2 byte char. \ is a valid second character. + */ + TV("GB18030", "\\\x81\\';"), + TV("GB18030", "\\\x81\\\";"), + TV("GB18030", "\\\x81\0;"), + TV("GB18030", "\\\x81\0'"), + TV("GB18030", "\\\x81'\0"), + + TV("SJIS", "\xF0\x40;"), + + TV("SJIS", "\xF0';"), + TV("SJIS", "\xF0\";"), + TV("SJIS", "\xF0\0'"), + TV("SJIS", "\\\xF0\\';"), + TV("SJIS", "\\\xF0\\\";"), + + TV("gbk", "\x80';"), + TV("gbk", "\x80"), + TV("gbk", "\x80'"), + TV("gbk", "\x80\""), + TV("gbk", "\x80\\"), + + TV("mule_internal", "\\\x9c';\0;"), + + TV("sql_ascii", "1\xC0'"), +}; + + +/* + * Print the string into buf, making characters outside of plain ascii + * somewhat easier to recognize. + * + * The output format could stand to be improved significantly, it's not at all + * unambiguous. + */ +static void +escapify(PQExpBuffer buf, const char *str, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + char c = *str; + + if (c == '\n') + appendPQExpBufferStr(buf, "\\n"); + else if (c == '\0') + appendPQExpBufferStr(buf, "\\0"); + else if (c < ' ' || c > '~') + appendPQExpBuffer(buf, "\\x%2x", (uint8_t) c); + else + appendPQExpBufferChar(buf, c); + str++; + } +} + +static void +report_result(pe_test_config *tc, + bool success, + PQExpBuffer testname, + PQExpBuffer details, + const char *subname, + const char *resultdesc) +{ + int test_id = ++tc->test_count; + bool print_details = true; + bool print_result = true; + + if (success) + { + if (tc->verbosity <= 0) + print_details = false; + if (tc->verbosity < 0) + print_result = false; + } + else + tc->failure_count++; + + if (print_details) + printf("%s", details->data); + + if (print_result) + printf("%s %d - %s: %s: %s\n", + success ? "ok" : "not ok", + test_id, testname->data, + subname, + resultdesc); +} + +/* + * Return true for encodings in which bytes in a multi-byte character look + * like valid ascii characters. + */ +static bool +encoding_conflicts_ascii(int encoding) +{ + /* + * We don't store this property directly anywhere, but whether an encoding + * is a client-only encoding is a good proxy. + */ + if (encoding > PG_ENCODING_BE_LAST) + return true; + return false; +} + +static const char * +scan_res_s(PsqlScanResult res) +{ +#define TOSTR_CASE(sym) case sym: return #sym + + switch (res) + { + TOSTR_CASE(PSCAN_SEMICOLON); + TOSTR_CASE(PSCAN_BACKSLASH); + TOSTR_CASE(PSCAN_INCOMPLETE); + TOSTR_CASE(PSCAN_EOL); + } + + pg_unreachable(); + return ""; /* silence compiler */ +} + +/* + * Verify that psql parses the input as a single statement. If this property + * is violated, the escape function does not effectively protect against + * smuggling in a second statement. + */ +static void +test_psql_parse(pe_test_config *tc, PQExpBuffer testname, + PQExpBuffer input_buf, PQExpBuffer details) +{ + PsqlScanState scan_state; + PsqlScanResult scan_result; + PQExpBuffer query_buf; + promptStatus_t prompt_status = PROMPT_READY; + int matches = 0; + bool test_fails; + const char *resdesc; + + query_buf = createPQExpBuffer(); + + scan_state = psql_scan_create(&test_scan_callbacks); + + /* + * TODO: This hardcodes standard conforming strings, it would be useful to + * test without as well. + */ + psql_scan_setup(scan_state, input_buf->data, input_buf->len, + PQclientEncoding(tc->conn), 1); + + do + { + resetPQExpBuffer(query_buf); + + scan_result = psql_scan(scan_state, query_buf, + &prompt_status); + + appendPQExpBuffer(details, + "#\t\t %d: scan_result: %s prompt: %u, query_buf: ", + matches, scan_res_s(scan_result), prompt_status); + escapify(details, query_buf->data, query_buf->len); + appendPQExpBuffer(details, "\n"); + + matches++; + } + while (scan_result != PSCAN_INCOMPLETE && scan_result != PSCAN_EOL); + + psql_scan_destroy(scan_state); + destroyPQExpBuffer(query_buf); + + test_fails = matches > 1 || scan_result != PSCAN_EOL; + + if (matches > 1) + resdesc = "more than one match"; + else if (scan_result != PSCAN_EOL) + resdesc = "unexpected end state"; + else + resdesc = "ok"; + + report_result(tc, !test_fails, testname, details, + "psql parse", + resdesc); +} + +static void +test_one_vector_escape(pe_test_config *tc, const pe_test_vector *tv, const pe_test_escape_func *ef) +{ + PQExpBuffer testname; + PQExpBuffer details; + PQExpBuffer escape_buf; + PQExpBuffer escape_err; + size_t input_encoding_validlen; + bool input_encoding_valid; + size_t input_encoding0_validlen; + bool input_encoding0_valid; + bool escape_success; + size_t escape_encoding_length; + bool escape_encoding_valid; + + escape_err = createPQExpBuffer(); + testname = createPQExpBuffer(); + details = createPQExpBuffer(); + escape_buf = createPQExpBuffer(); + + if (ef->supports_only_ascii_overlap && + encoding_conflicts_ascii(PQclientEncoding(tc->conn))) + { + goto out; + } + + /* name to describe the test */ + appendPQExpBuffer(testname, ">"); + escapify(testname, tv->escape, tv->escape_len); + appendPQExpBuffer(testname, "< - %s - %s", + tv->client_encoding, ef->name); + + /* details to describe the test, to allow for debugging */ + appendPQExpBuffer(details, "#\t input: %zd bytes: ", + tv->escape_len); + escapify(details, tv->escape, tv->escape_len); + appendPQExpBufferStr(details, "\n"); + appendPQExpBuffer(details, "#\t encoding: %s\n", + tv->client_encoding); + + + /* check encoding of input, to compare with after the test */ + input_encoding_validlen = pg_encoding_verifymbstr(PQclientEncoding(tc->conn), + tv->escape, + tv->escape_len); + input_encoding_valid = input_encoding_validlen == tv->escape_len; + appendPQExpBuffer(details, "#\t input encoding valid: %d\n", + input_encoding_valid); + + input_encoding0_validlen = pg_encoding_verifymbstr(PQclientEncoding(tc->conn), + tv->escape, + strlen(tv->escape)); + input_encoding0_valid = input_encoding0_validlen == strlen(tv->escape); + appendPQExpBuffer(details, "#\t input encoding valid till 0: %d\n", + input_encoding0_valid); + + appendPQExpBuffer(details, "#\t escape func: %s\n", + ef->name); + + if (!input_encoding_valid && ef->supports_only_valid + && !tc->force_unsupported) + goto out; + + + /* call the to-be-tested escape function */ + escape_success = ef->escape(tc->conn, escape_buf, + tv->escape, tv->escape_len, + escape_err); + if (!escape_success) + { + appendPQExpBuffer(details, "#\t escape error: %s\n", + escape_err->data); + } + + if (escape_buf->len > 0) + { + appendPQExpBuffer(details, "#\t escaped string: %zd bytes: ", escape_buf->len); + escapify(details, escape_buf->data, escape_buf->len); + appendPQExpBufferChar(details, '\n'); + + escape_encoding_length = pg_encoding_verifymbstr(PQclientEncoding(tc->conn), + escape_buf->data, + escape_buf->len); + escape_encoding_valid = escape_encoding_length == escape_buf->len; + + appendPQExpBuffer(details, "#\t escape encoding valid: %d\n", + escape_encoding_valid); + } + else + { + escape_encoding_length = 0; + escape_encoding_valid = 1; + } + + /* + * If the test reports errors, and the input was invalidly encoded, + * escaping should fail. One edge-case that we accept for now is that the + * input could have an embedded null byte, which the escape functions will + * just treat as a shorter string. If the encoding error is after the zero + * byte, the output thus won't contain it. + */ + if (ef->reports_errors) + { + bool ok = true; + const char *resdesc = "ok"; + + if (escape_success) + { + if (!input_encoding0_valid) + { + ok = false; + resdesc = "invalid input escaped successfully"; + } + else if (!input_encoding_valid) + resdesc = "invalid input escaped successfully, due to zero byte"; + } + else + { + if (input_encoding0_valid) + { + ok = false; + resdesc = "valid input failed to escape"; + } + else if (input_encoding_valid) + resdesc = "valid input failed to escape, due to zero byte"; + } + + report_result(tc, ok, testname, details, + "input validity vs escape success", + resdesc); + } + + /* + * If the input is invalidly encoded, the output should also be invalidly + * encoded. We accept the same zero-byte edge case as above. + */ + { + bool ok = true; + const char *resdesc = "ok"; + + if (input_encoding0_valid && !input_encoding_valid && escape_encoding_valid) + { + resdesc = "invalid input produced valid output, due to zero byte"; + } + else if (input_encoding0_valid && !escape_encoding_valid) + { + ok = false; + resdesc = "valid input produced invalid output"; + } + else if (!input_encoding0_valid && + (!ef->reports_errors || escape_success) && + escape_encoding_valid) + { + ok = false; + resdesc = "invalid input produced valid output"; + } + + report_result(tc, ok, testname, details, + "input and escaped encoding validity", + resdesc); + } + + /* + * Test psql parsing whenever we get any string back, even if the escape + * function returned a failure. + */ + if (escape_buf->len > 0) + { + test_psql_parse(tc, testname, + escape_buf, details); + } + +out: + destroyPQExpBuffer(escape_err); + destroyPQExpBuffer(details); + destroyPQExpBuffer(testname); + destroyPQExpBuffer(escape_buf); +} + +static void +test_one_vector(pe_test_config *tc, const pe_test_vector *tv) +{ + if (PQsetClientEncoding(tc->conn, tv->client_encoding)) + { + fprintf(stderr, "failed to set encoding to %s:\n%s\n", + tv->client_encoding, PQerrorMessage(tc->conn)); + exit(1); + } + + for (int escoff = 0; escoff < lengthof(pe_test_escape_funcs); escoff++) + { + const pe_test_escape_func *ef = &pe_test_escape_funcs[escoff]; + + test_one_vector_escape(tc, tv, ef); + } +} + +static void +usage(const char *hint) +{ + if (hint) + fprintf(stderr, "Error: %s\n\n", hint); + + printf("PostgreSQL escape function test\n" + "\n" + "Usage:\n" + " test_escape --conninfo=CONNINFO [OPTIONS]\n" + "\n" + "Options:\n" + " -h, --help show this help\n" + " -c, --conninfo=CONNINFO connection information to use\n" + " -v, --verbose show test details even for successes\n" + " -q, --quiet only show failures\n" + " --force-unsupported test invalid input even if unsupported\n" + ); + + if (hint) + exit(1); +} + +int +main(int argc, char *argv[]) +{ + pe_test_config tc = {0}; + char c; + int option_index; + + static const struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"conninfo", required_argument, NULL, 'c'}, + {"verbose", no_argument, NULL, 'v'}, + {"quiet", no_argument, NULL, 'q'}, + {"force-unsupported", no_argument, NULL, 'f'}, + {NULL, 0, NULL, 0}, + }; + + while ((c = getopt_long(argc, argv, "vqh", long_options, &option_index)) != -1) + { + switch (c) + { + case 'h': + usage(NULL); + exit(0); + break; + case 'c': + tc.conninfo = optarg; + break; + case 'v': + tc.verbosity++; + break; + case 'q': + tc.verbosity--; + break; + case 'f': + tc.force_unsupported = true; + break; + } + } + + if (argc - optind >= 1) + usage("unused option(s) specified"); + + if (tc.conninfo == NULL) + usage("--conninfo needs to be specified"); + + tc.conn = PQconnectdb(tc.conninfo); + + if (!tc.conn || PQstatus(tc.conn) != CONNECTION_OK) + { + fprintf(stderr, "could not connect: %s\n", + PQerrorMessage(tc.conn)); + exit(1); + } + + for (int i = 0; i < lengthof(pe_test_vectors); i++) + { + test_one_vector(&tc, &pe_test_vectors[i]); + } + + PQfinish(tc.conn); + + printf("# %d failures\n", tc.failure_count); + printf("1..%d\n", tc.test_count); + return tc.failure_count > 0; +} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f522be3ecf1..72e6d3a9865 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3662,6 +3662,9 @@ pam_handle_t parallel_worker_main_type parse_error_callback_arg partition_method_t +pe_test_config +pe_test_escape_func +pe_test_vector pendingPosition pending_label pgParameterStatus |