From 5e45211a64149b3c659b90ff2de6fa982a5a93ed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 14:17:33 +0200 Subject: Adding upstream version 15.5. Signed-off-by: Daniel Baumann --- src/fe_utils/string_utils.c | 1233 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1233 insertions(+) create mode 100644 src/fe_utils/string_utils.c (limited to 'src/fe_utils/string_utils.c') diff --git a/src/fe_utils/string_utils.c b/src/fe_utils/string_utils.c new file mode 100644 index 0000000..f311bdd --- /dev/null +++ b/src/fe_utils/string_utils.c @@ -0,0 +1,1233 @@ +/*------------------------------------------------------------------------- + * + * String-processing utility routines for frontend code + * + * Assorted utility functions that are useful in constructing SQL queries + * and interpreting backend output. + * + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/fe_utils/string_utils.c + * + *------------------------------------------------------------------------- + */ +#include "postgres_fe.h" + +#include + +#include "common/keywords.h" +#include "fe_utils/string_utils.h" + +static PQExpBuffer defaultGetLocalPQExpBuffer(void); + +/* Globals exported by this file */ +int quote_all_identifiers = 0; +PQExpBuffer (*getLocalPQExpBuffer) (void) = defaultGetLocalPQExpBuffer; + + +/* + * Returns a temporary PQExpBuffer, valid until the next call to the function. + * This is used by fmtId and fmtQualifiedId. + * + * Non-reentrant and non-thread-safe but reduces memory leakage. You can + * replace this with a custom version by setting the getLocalPQExpBuffer + * function pointer. + */ +static PQExpBuffer +defaultGetLocalPQExpBuffer(void) +{ + static PQExpBuffer id_return = NULL; + + if (id_return) /* first time through? */ + { + /* same buffer, just wipe contents */ + resetPQExpBuffer(id_return); + } + else + { + /* new buffer */ + id_return = createPQExpBuffer(); + } + + return id_return; +} + +/* + * Quotes input string if it's not a legitimate SQL identifier as-is. + * + * Note that the returned string must be used before calling fmtId again, + * since we re-use the same return buffer each time. + */ +const char * +fmtId(const char *rawid) +{ + PQExpBuffer id_return = getLocalPQExpBuffer(); + + const char *cp; + bool need_quotes = false; + + /* + * These checks need to match the identifier production in scan.l. Don't + * use islower() etc. + */ + if (quote_all_identifiers) + need_quotes = true; + /* slightly different rules for first character */ + else if (!((rawid[0] >= 'a' && rawid[0] <= 'z') || rawid[0] == '_')) + need_quotes = true; + else + { + /* otherwise check the entire string */ + for (cp = rawid; *cp; cp++) + { + if (!((*cp >= 'a' && *cp <= 'z') + || (*cp >= '0' && *cp <= '9') + || (*cp == '_'))) + { + need_quotes = true; + break; + } + } + } + + if (!need_quotes) + { + /* + * Check for keyword. We quote keywords except for unreserved ones. + * (In some cases we could avoid quoting a col_name or type_func_name + * keyword, but it seems much harder than it's worth to tell that.) + * + * Note: ScanKeywordLookup() does case-insensitive comparison, but + * that's fine, since we already know we have all-lower-case. + */ + int kwnum = ScanKeywordLookup(rawid, &ScanKeywords); + + if (kwnum >= 0 && ScanKeywordCategories[kwnum] != UNRESERVED_KEYWORD) + need_quotes = true; + } + + if (!need_quotes) + { + /* no quoting needed */ + appendPQExpBufferStr(id_return, rawid); + } + else + { + appendPQExpBufferChar(id_return, '"'); + for (cp = rawid; *cp; cp++) + { + /* + * Did we find a double-quote in the string? Then make this a + * double double-quote per SQL99. Before, we put in a + * backslash/double-quote pair. - thomas 2000-08-05 + */ + if (*cp == '"') + appendPQExpBufferChar(id_return, '"'); + appendPQExpBufferChar(id_return, *cp); + } + appendPQExpBufferChar(id_return, '"'); + } + + return id_return->data; +} + +/* + * fmtQualifiedId - construct a schema-qualified name, with quoting as needed. + * + * Like fmtId, use the result before calling again. + * + * Since we call fmtId and it also uses getLocalPQExpBuffer() we cannot + * use that buffer until we're finished with calling fmtId(). + */ +const char * +fmtQualifiedId(const char *schema, const char *id) +{ + PQExpBuffer id_return; + PQExpBuffer lcl_pqexp = createPQExpBuffer(); + + /* Some callers might fail to provide a schema name */ + if (schema && *schema) + { + appendPQExpBuffer(lcl_pqexp, "%s.", fmtId(schema)); + } + appendPQExpBufferStr(lcl_pqexp, fmtId(id)); + + id_return = getLocalPQExpBuffer(); + + appendPQExpBufferStr(id_return, lcl_pqexp->data); + destroyPQExpBuffer(lcl_pqexp); + + return id_return->data; +} + + +/* + * Format a Postgres version number (in the PG_VERSION_NUM integer format + * returned by PQserverVersion()) as a string. This exists mainly to + * encapsulate knowledge about two-part vs. three-part version numbers. + * + * For reentrancy, caller must supply the buffer the string is put in. + * Recommended size of the buffer is 32 bytes. + * + * Returns address of 'buf', as a notational convenience. + */ +char * +formatPGVersionNumber(int version_number, bool include_minor, + char *buf, size_t buflen) +{ + if (version_number >= 100000) + { + /* New two-part style */ + if (include_minor) + snprintf(buf, buflen, "%d.%d", version_number / 10000, + version_number % 10000); + else + snprintf(buf, buflen, "%d", version_number / 10000); + } + else + { + /* Old three-part style */ + if (include_minor) + snprintf(buf, buflen, "%d.%d.%d", version_number / 10000, + (version_number / 100) % 100, + version_number % 100); + else + snprintf(buf, buflen, "%d.%d", version_number / 10000, + (version_number / 100) % 100); + } + return buf; +} + + +/* + * Convert a string value to an SQL string literal and append it to + * the given buffer. We assume the specified client_encoding and + * standard_conforming_strings settings. + * + * This is essentially equivalent to libpq's PQescapeStringInternal, + * except for the output buffer structure. We need it in situations + * where we do not have a PGconn available. Where we do, + * appendStringLiteralConn is a better choice. + */ +void +appendStringLiteral(PQExpBuffer buf, const char *str, + int encoding, bool std_strings) +{ + size_t length = strlen(str); + const char *source = str; + char *target; + + if (!enlargePQExpBuffer(buf, 2 * length + 2)) + return; + + target = buf->data + buf->len; + *target++ = '\''; + + while (*source != '\0') + { + char c = *source; + int len; + int i; + + /* Fast path for plain ASCII */ + if (!IS_HIGHBIT_SET(c)) + { + /* Apply quoting if needed */ + if (SQL_STR_DOUBLE(c, !std_strings)) + *target++ = c; + /* Copy the character */ + *target++ = c; + source++; + continue; + } + + /* Slow path for possible multibyte characters */ + len = PQmblen(source, encoding); + + /* Copy the character */ + for (i = 0; i < len; i++) + { + if (*source == '\0') + break; + *target++ = *source++; + } + + /* + * If we hit premature end of string (ie, incomplete multibyte + * character), try to pad out to the correct length with spaces. We + * may not be able to pad completely, but we will always be able to + * insert at least one pad space (since we'd not have quoted a + * multibyte character). This should be enough to make a string that + * the server will error out on. + */ + if (i < len) + { + char *stop = buf->data + buf->maxlen - 2; + + for (; i < len; i++) + { + if (target >= stop) + break; + *target++ = ' '; + } + break; + } + } + + /* Write the terminating quote and NUL character. */ + *target++ = '\''; + *target = '\0'; + + buf->len = target - buf->data; +} + + +/* + * Convert a string value to an SQL string literal and append it to + * the given buffer. Encoding and string syntax rules are as indicated + * by current settings of the PGconn. + */ +void +appendStringLiteralConn(PQExpBuffer buf, const char *str, PGconn *conn) +{ + size_t length = strlen(str); + + /* + * XXX This is a kluge to silence escape_string_warning in our utility + * programs. It should go away someday. + */ + if (strchr(str, '\\') != NULL && PQserverVersion(conn) >= 80100) + { + /* ensure we are not adjacent to an identifier */ + if (buf->len > 0 && buf->data[buf->len - 1] != ' ') + appendPQExpBufferChar(buf, ' '); + appendPQExpBufferChar(buf, ESCAPE_STRING_SYNTAX); + appendStringLiteral(buf, str, PQclientEncoding(conn), false); + return; + } + /* XXX end kluge */ + + if (!enlargePQExpBuffer(buf, 2 * length + 2)) + return; + appendPQExpBufferChar(buf, '\''); + buf->len += PQescapeStringConn(conn, buf->data + buf->len, + str, length, NULL); + appendPQExpBufferChar(buf, '\''); +} + + +/* + * Convert a string value to a dollar quoted literal and append it to + * the given buffer. If the dqprefix parameter is not NULL then the + * dollar quote delimiter will begin with that (after the opening $). + * + * No escaping is done at all on str, in compliance with the rules + * for parsing dollar quoted strings. Also, we need not worry about + * encoding issues. + */ +void +appendStringLiteralDQ(PQExpBuffer buf, const char *str, const char *dqprefix) +{ + static const char suffixes[] = "_XXXXXXX"; + int nextchar = 0; + PQExpBuffer delimBuf = createPQExpBuffer(); + + /* start with $ + dqprefix if not NULL */ + appendPQExpBufferChar(delimBuf, '$'); + if (dqprefix) + appendPQExpBufferStr(delimBuf, dqprefix); + + /* + * Make sure we choose a delimiter which (without the trailing $) is not + * present in the string being quoted. We don't check with the trailing $ + * because a string ending in $foo must not be quoted with $foo$. + */ + while (strstr(str, delimBuf->data) != NULL) + { + appendPQExpBufferChar(delimBuf, suffixes[nextchar++]); + nextchar %= sizeof(suffixes) - 1; + } + + /* add trailing $ */ + appendPQExpBufferChar(delimBuf, '$'); + + /* quote it and we are all done */ + appendPQExpBufferStr(buf, delimBuf->data); + appendPQExpBufferStr(buf, str); + appendPQExpBufferStr(buf, delimBuf->data); + + destroyPQExpBuffer(delimBuf); +} + + +/* + * Convert a bytea value (presented as raw bytes) to an SQL string literal + * and append it to the given buffer. We assume the specified + * standard_conforming_strings setting. + * + * This is needed in situations where we do not have a PGconn available. + * Where we do, PQescapeByteaConn is a better choice. + */ +void +appendByteaLiteral(PQExpBuffer buf, const unsigned char *str, size_t length, + bool std_strings) +{ + const unsigned char *source = str; + char *target; + + static const char hextbl[] = "0123456789abcdef"; + + /* + * This implementation is hard-wired to produce hex-format output. We do + * not know the server version the output will be loaded into, so making + * an intelligent format choice is impossible. It might be better to + * always use the old escaped format. + */ + if (!enlargePQExpBuffer(buf, 2 * length + 5)) + return; + + target = buf->data + buf->len; + *target++ = '\''; + if (!std_strings) + *target++ = '\\'; + *target++ = '\\'; + *target++ = 'x'; + + while (length-- > 0) + { + unsigned char c = *source++; + + *target++ = hextbl[(c >> 4) & 0xF]; + *target++ = hextbl[c & 0xF]; + } + + /* Write the terminating quote and NUL character. */ + *target++ = '\''; + *target = '\0'; + + buf->len = target - buf->data; +} + + +/* + * Append the given string to the shell command being built in the buffer, + * with shell-style quoting as needed to create exactly one argument. + * + * Forbid LF or CR characters, which have scant practical use beyond designing + * security breaches. The Windows command shell is unusable as a conduit for + * arguments containing LF or CR characters. A future major release should + * reject those characters in CREATE ROLE and CREATE DATABASE, because use + * there eventually leads to errors here. + * + * appendShellString() simply prints an error and dies if LF or CR appears. + * appendShellStringNoError() omits those characters from the result, and + * returns false if there were any. + */ +void +appendShellString(PQExpBuffer buf, const char *str) +{ + if (!appendShellStringNoError(buf, str)) + { + fprintf(stderr, + _("shell command argument contains a newline or carriage return: \"%s\"\n"), + str); + exit(EXIT_FAILURE); + } +} + +bool +appendShellStringNoError(PQExpBuffer buf, const char *str) +{ +#ifdef WIN32 + int backslash_run_length = 0; +#endif + bool ok = true; + const char *p; + + /* + * Don't bother with adding quotes if the string is nonempty and clearly + * contains only safe characters. + */ + if (*str != '\0' && + strspn(str, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./:") == strlen(str)) + { + appendPQExpBufferStr(buf, str); + return ok; + } + +#ifndef WIN32 + appendPQExpBufferChar(buf, '\''); + for (p = str; *p; p++) + { + if (*p == '\n' || *p == '\r') + { + ok = false; + continue; + } + + if (*p == '\'') + appendPQExpBufferStr(buf, "'\"'\"'"); + else + appendPQExpBufferChar(buf, *p); + } + appendPQExpBufferChar(buf, '\''); +#else /* WIN32 */ + + /* + * A Windows system() argument experiences two layers of interpretation. + * First, cmd.exe interprets the string. Its behavior is undocumented, + * but a caret escapes any byte except LF or CR that would otherwise have + * special meaning. Handling of a caret before LF or CR differs between + * "cmd.exe /c" and other modes, and it is unusable here. + * + * Second, the new process parses its command line to construct argv (see + * https://msdn.microsoft.com/en-us/library/17w5ykft.aspx). This treats + * backslash-double quote sequences specially. + */ + appendPQExpBufferStr(buf, "^\""); + for (p = str; *p; p++) + { + if (*p == '\n' || *p == '\r') + { + ok = false; + continue; + } + + /* Change N backslashes before a double quote to 2N+1 backslashes. */ + if (*p == '"') + { + while (backslash_run_length) + { + appendPQExpBufferStr(buf, "^\\"); + backslash_run_length--; + } + appendPQExpBufferStr(buf, "^\\"); + } + else if (*p == '\\') + backslash_run_length++; + else + backslash_run_length = 0; + + /* + * Decline to caret-escape the most mundane characters, to ease + * debugging and lest we approach the command length limit. + */ + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + appendPQExpBufferChar(buf, '^'); + appendPQExpBufferChar(buf, *p); + } + + /* + * Change N backslashes at end of argument to 2N backslashes, because they + * precede the double quote that terminates the argument. + */ + while (backslash_run_length) + { + appendPQExpBufferStr(buf, "^\\"); + backslash_run_length--; + } + appendPQExpBufferStr(buf, "^\""); +#endif /* WIN32 */ + + return ok; +} + + +/* + * Append the given string to the buffer, with suitable quoting for passing + * the string as a value in a keyword/value pair in a libpq connection string. + */ +void +appendConnStrVal(PQExpBuffer buf, const char *str) +{ + const char *s; + bool needquotes; + + /* + * If the string is one or more plain ASCII characters, no need to quote + * it. This is quite conservative, but better safe than sorry. + */ + needquotes = true; + for (s = str; *s; s++) + { + if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || + (*s >= '0' && *s <= '9') || *s == '_' || *s == '.')) + { + needquotes = true; + break; + } + needquotes = false; + } + + if (needquotes) + { + appendPQExpBufferChar(buf, '\''); + while (*str) + { + /* ' and \ must be escaped by to \' and \\ */ + if (*str == '\'' || *str == '\\') + appendPQExpBufferChar(buf, '\\'); + + appendPQExpBufferChar(buf, *str); + str++; + } + appendPQExpBufferChar(buf, '\''); + } + else + appendPQExpBufferStr(buf, str); +} + + +/* + * Append a psql meta-command that connects to the given database with the + * then-current connection's user, host and port. + */ +void +appendPsqlMetaConnect(PQExpBuffer buf, const char *dbname) +{ + const char *s; + bool complex; + + /* + * If the name is plain ASCII characters, emit a trivial "\connect "foo"". + * For other names, even many not technically requiring it, skip to the + * general case. No database has a zero-length name. + */ + complex = false; + + for (s = dbname; *s; s++) + { + if (*s == '\n' || *s == '\r') + { + fprintf(stderr, + _("database name contains a newline or carriage return: \"%s\"\n"), + dbname); + exit(EXIT_FAILURE); + } + + if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || + (*s >= '0' && *s <= '9') || *s == '_' || *s == '.')) + { + complex = true; + } + } + + appendPQExpBufferStr(buf, "\\connect "); + if (complex) + { + PQExpBufferData connstr; + + initPQExpBuffer(&connstr); + appendPQExpBufferStr(&connstr, "dbname="); + appendConnStrVal(&connstr, dbname); + + appendPQExpBufferStr(buf, "-reuse-previous=on "); + + /* + * As long as the name does not contain a newline, SQL identifier + * quoting satisfies the psql meta-command parser. Prefer not to + * involve psql-interpreted single quotes, which behaved differently + * before PostgreSQL 9.2. + */ + appendPQExpBufferStr(buf, fmtId(connstr.data)); + + termPQExpBuffer(&connstr); + } + else + appendPQExpBufferStr(buf, fmtId(dbname)); + appendPQExpBufferChar(buf, '\n'); +} + + +/* + * Deconstruct the text representation of a 1-dimensional Postgres array + * into individual items. + * + * On success, returns true and sets *itemarray and *nitems to describe + * an array of individual strings. On parse failure, returns false; + * *itemarray may exist or be NULL. + * + * NOTE: free'ing itemarray is sufficient to deallocate the working storage. + */ +bool +parsePGArray(const char *atext, char ***itemarray, int *nitems) +{ + int inputlen; + char **items; + char *strings; + int curitem; + + /* + * We expect input in the form of "{item,item,item}" where any item is + * either raw data, or surrounded by double quotes (in which case embedded + * characters including backslashes and quotes are backslashed). + * + * We build the result as an array of pointers followed by the actual + * string data, all in one malloc block for convenience of deallocation. + * The worst-case storage need is not more than one pointer and one + * character for each input character (consider "{,,,,,,,,,,}"). + */ + *itemarray = NULL; + *nitems = 0; + inputlen = strlen(atext); + if (inputlen < 2 || atext[0] != '{' || atext[inputlen - 1] != '}') + return false; /* bad input */ + items = (char **) malloc(inputlen * (sizeof(char *) + sizeof(char))); + if (items == NULL) + return false; /* out of memory */ + *itemarray = items; + strings = (char *) (items + inputlen); + + atext++; /* advance over initial '{' */ + curitem = 0; + while (*atext != '}') + { + if (*atext == '\0') + return false; /* premature end of string */ + items[curitem] = strings; + while (*atext != '}' && *atext != ',') + { + if (*atext == '\0') + return false; /* premature end of string */ + if (*atext != '"') + *strings++ = *atext++; /* copy unquoted data */ + else + { + /* process quoted substring */ + atext++; + while (*atext != '"') + { + if (*atext == '\0') + return false; /* premature end of string */ + if (*atext == '\\') + { + atext++; + if (*atext == '\0') + return false; /* premature end of string */ + } + *strings++ = *atext++; /* copy quoted data */ + } + atext++; + } + } + *strings++ = '\0'; + if (*atext == ',') + atext++; + curitem++; + } + if (atext[1] != '\0') + return false; /* bogus syntax (embedded '}') */ + *nitems = curitem; + return true; +} + + +/* + * Append one element to the text representation of a 1-dimensional Postgres + * array. + * + * The caller must provide the initial '{' and closing '}' of the array. + * This function handles all else, including insertion of commas and + * quoting of values. + * + * We assume that typdelim is ','. + */ +void +appendPGArray(PQExpBuffer buffer, const char *value) +{ + bool needquote; + const char *tmp; + + if (buffer->data[buffer->len - 1] != '{') + appendPQExpBufferChar(buffer, ','); + + /* Decide if we need quotes; this should match array_out()'s choices. */ + if (value[0] == '\0') + needquote = true; /* force quotes for empty string */ + else if (pg_strcasecmp(value, "NULL") == 0) + needquote = true; /* force quotes for literal NULL */ + else + needquote = false; + + if (!needquote) + { + for (tmp = value; *tmp; tmp++) + { + char ch = *tmp; + + if (ch == '"' || ch == '\\' || + ch == '{' || ch == '}' || ch == ',' || + /* these match array_isspace(): */ + ch == ' ' || ch == '\t' || ch == '\n' || + ch == '\r' || ch == '\v' || ch == '\f') + { + needquote = true; + break; + } + } + } + + if (needquote) + { + appendPQExpBufferChar(buffer, '"'); + for (tmp = value; *tmp; tmp++) + { + char ch = *tmp; + + if (ch == '"' || ch == '\\') + appendPQExpBufferChar(buffer, '\\'); + appendPQExpBufferChar(buffer, ch); + } + appendPQExpBufferChar(buffer, '"'); + } + else + appendPQExpBufferStr(buffer, value); +} + + +/* + * Format a reloptions array and append it to the given buffer. + * + * "prefix" is prepended to the option names; typically it's "" or "toast.". + * + * Returns false if the reloptions array could not be parsed (in which case + * nothing will have been appended to the buffer), or true on success. + * + * Note: this logic should generally match the backend's flatten_reloptions() + * (in adt/ruleutils.c). + */ +bool +appendReloptionsArray(PQExpBuffer buffer, const char *reloptions, + const char *prefix, int encoding, bool std_strings) +{ + char **options; + int noptions; + int i; + + if (!parsePGArray(reloptions, &options, &noptions)) + { + if (options) + free(options); + return false; + } + + for (i = 0; i < noptions; i++) + { + char *option = options[i]; + char *name; + char *separator; + char *value; + + /* + * Each array element should have the form name=value. If the "=" is + * missing for some reason, treat it like an empty value. + */ + name = option; + separator = strchr(option, '='); + if (separator) + { + *separator = '\0'; + value = separator + 1; + } + else + value = ""; + + if (i > 0) + appendPQExpBufferStr(buffer, ", "); + appendPQExpBuffer(buffer, "%s%s=", prefix, fmtId(name)); + + /* + * In general we need to quote the value; but to avoid unnecessary + * clutter, do not quote if it is an identifier that would not need + * quoting. (We could also allow numbers, but that is a bit trickier + * than it looks --- for example, are leading zeroes significant? We + * don't want to assume very much here about what custom reloptions + * might mean.) + */ + if (strcmp(fmtId(value), value) == 0) + appendPQExpBufferStr(buffer, value); + else + appendStringLiteral(buffer, value, encoding, std_strings); + } + + if (options) + free(options); + + return true; +} + + +/* + * processSQLNamePattern + * + * Scan a wildcard-pattern string and generate appropriate WHERE clauses + * to limit the set of objects returned. The WHERE clauses are appended + * to the already-partially-constructed query in buf. Returns whether + * any clause was added. + * + * conn: connection query will be sent to (consulted for escaping rules). + * buf: output parameter. + * pattern: user-specified pattern option, or NULL if none ("*" is implied). + * have_where: true if caller already emitted "WHERE" (clauses will be ANDed + * onto the existing WHERE clause). + * force_escape: always quote regexp special characters, even outside + * double quotes (else they are quoted only between double quotes). + * schemavar: name of query variable to match against a schema-name pattern. + * Can be NULL if no schema. + * namevar: name of query variable to match against an object-name pattern. + * altnamevar: NULL, or name of an alternative variable to match against name. + * visibilityrule: clause to use if we want to restrict to visible objects + * (for example, "pg_catalog.pg_table_is_visible(p.oid)"). Can be NULL. + * dbnamebuf: output parameter receiving the database name portion of the + * pattern, if any. Can be NULL. + * dotcnt: how many separators were parsed from the pattern, by reference. + * + * Formatting note: the text already present in buf should end with a newline. + * The appended text, if any, will end with one too. + */ +bool +processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern, + bool have_where, bool force_escape, + const char *schemavar, const char *namevar, + const char *altnamevar, const char *visibilityrule, + PQExpBuffer dbnamebuf, int *dotcnt) +{ + PQExpBufferData schemabuf; + PQExpBufferData namebuf; + bool added_clause = false; + int dcnt; + +#define WHEREAND() \ + (appendPQExpBufferStr(buf, have_where ? " AND " : "WHERE "), \ + have_where = true, added_clause = true) + + if (dotcnt == NULL) + dotcnt = &dcnt; + *dotcnt = 0; + if (pattern == NULL) + { + /* Default: select all visible objects */ + if (visibilityrule) + { + WHEREAND(); + appendPQExpBuffer(buf, "%s\n", visibilityrule); + } + return added_clause; + } + + initPQExpBuffer(&schemabuf); + initPQExpBuffer(&namebuf); + + /* + * Convert shell-style 'pattern' into the regular expression(s) we want to + * execute. Quoting/escaping into SQL literal format will be done below + * using appendStringLiteralConn(). + * + * If the caller provided a schemavar, we want to split the pattern on + * ".", otherwise not. + */ + patternToSQLRegex(PQclientEncoding(conn), + (schemavar ? dbnamebuf : NULL), + (schemavar ? &schemabuf : NULL), + &namebuf, + pattern, force_escape, true, dotcnt); + + /* + * Now decide what we need to emit. We may run under a hostile + * search_path, so qualify EVERY name. Note there will be a leading "^(" + * in the patterns in any case. + * + * We want the regex matches to use the database's default collation where + * collation-sensitive behavior is required (for example, which characters + * match '\w'). That happened by default before PG v12, but if the server + * is >= v12 then we need to force it through explicit COLLATE clauses, + * otherwise the "C" collation attached to "name" catalog columns wins. + */ + if (namevar && namebuf.len > 2) + { + /* We have a name pattern, so constrain the namevar(s) */ + + /* Optimize away a "*" pattern */ + if (strcmp(namebuf.data, "^(.*)$") != 0) + { + WHEREAND(); + if (altnamevar) + { + appendPQExpBuffer(buf, + "(%s OPERATOR(pg_catalog.~) ", namevar); + appendStringLiteralConn(buf, namebuf.data, conn); + if (PQserverVersion(conn) >= 120000) + appendPQExpBufferStr(buf, " COLLATE pg_catalog.default"); + appendPQExpBuffer(buf, + "\n OR %s OPERATOR(pg_catalog.~) ", + altnamevar); + appendStringLiteralConn(buf, namebuf.data, conn); + if (PQserverVersion(conn) >= 120000) + appendPQExpBufferStr(buf, " COLLATE pg_catalog.default"); + appendPQExpBufferStr(buf, ")\n"); + } + else + { + appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", namevar); + appendStringLiteralConn(buf, namebuf.data, conn); + if (PQserverVersion(conn) >= 120000) + appendPQExpBufferStr(buf, " COLLATE pg_catalog.default"); + appendPQExpBufferChar(buf, '\n'); + } + } + } + + if (schemavar && schemabuf.len > 2) + { + /* We have a schema pattern, so constrain the schemavar */ + + /* Optimize away a "*" pattern */ + if (strcmp(schemabuf.data, "^(.*)$") != 0 && schemavar) + { + WHEREAND(); + appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", schemavar); + appendStringLiteralConn(buf, schemabuf.data, conn); + if (PQserverVersion(conn) >= 120000) + appendPQExpBufferStr(buf, " COLLATE pg_catalog.default"); + appendPQExpBufferChar(buf, '\n'); + } + } + else + { + /* No schema pattern given, so select only visible objects */ + if (visibilityrule) + { + WHEREAND(); + appendPQExpBuffer(buf, "%s\n", visibilityrule); + } + } + + termPQExpBuffer(&schemabuf); + termPQExpBuffer(&namebuf); + + return added_clause; +#undef WHEREAND +} + +/* + * Transform a possibly qualified shell-style object name pattern into up to + * three SQL-style regular expressions, converting quotes, lower-casing + * unquoted letters, and adjusting shell-style wildcard characters into regexp + * notation. + * + * If the dbnamebuf and schemabuf arguments are non-NULL, and the pattern + * contains two or more dbname/schema/name separators, we parse the portions of + * the pattern prior to the first and second separators into dbnamebuf and + * schemabuf, and the rest into namebuf. + * + * If dbnamebuf is NULL and schemabuf is non-NULL, and the pattern contains at + * least one separator, we parse the first portion into schemabuf and the rest + * into namebuf. + * + * Otherwise, we parse all the pattern into namebuf. + * + * If the pattern contains more dotted parts than buffers to parse into, the + * extra dots will be treated as literal characters and written into the + * namebuf, though they will be counted. Callers should always check the value + * returned by reference in dotcnt and handle this error case appropriately. + * + * We surround the regexps with "^(...)$" to force them to match whole strings, + * as per SQL practice. We have to have parens in case strings contain "|", + * else the "^" and "$" will be bound into the first and last alternatives + * which is not what we want. Whether this is done for dbnamebuf is controlled + * by the want_literal_dbname parameter. + * + * The regexps we parse into the buffers are appended to the data (if any) + * already present. If we parse fewer fields than the number of buffers we + * were given, the extra buffers are unaltered. + * + * encoding: the character encoding for the given pattern + * dbnamebuf: output parameter receiving the database name portion of the + * pattern, if any. Can be NULL. + * schemabuf: output parameter receiving the schema name portion of the + * pattern, if any. Can be NULL. + * namebuf: output parameter receiving the database name portion of the + * pattern, if any. Can be NULL. + * pattern: user-specified pattern option, or NULL if none ("*" is implied). + * force_escape: always quote regexp special characters, even outside + * double quotes (else they are quoted only between double quotes). + * want_literal_dbname: if true, regexp special characters within the database + * name portion of the pattern will not be escaped, nor will the dbname be + * converted into a regular expression. + * dotcnt: output parameter receiving the number of separators parsed from the + * pattern. + */ +void +patternToSQLRegex(int encoding, PQExpBuffer dbnamebuf, PQExpBuffer schemabuf, + PQExpBuffer namebuf, const char *pattern, bool force_escape, + bool want_literal_dbname, int *dotcnt) +{ + PQExpBufferData buf[3]; + PQExpBufferData left_literal; + PQExpBuffer curbuf; + PQExpBuffer maxbuf; + int i; + bool inquotes; + bool left; + const char *cp; + + Assert(pattern != NULL); + Assert(namebuf != NULL); + + /* callers should never expect "dbname.relname" format */ + Assert(dbnamebuf == NULL || schemabuf != NULL); + Assert(dotcnt != NULL); + + *dotcnt = 0; + inquotes = false; + cp = pattern; + + if (dbnamebuf != NULL) + maxbuf = &buf[2]; + else if (schemabuf != NULL) + maxbuf = &buf[1]; + else + maxbuf = &buf[0]; + + curbuf = &buf[0]; + if (want_literal_dbname) + { + left = true; + initPQExpBuffer(&left_literal); + } + else + left = false; + initPQExpBuffer(curbuf); + appendPQExpBufferStr(curbuf, "^("); + while (*cp) + { + char ch = *cp; + + if (ch == '"') + { + if (inquotes && cp[1] == '"') + { + /* emit one quote, stay in inquotes mode */ + appendPQExpBufferChar(curbuf, '"'); + if (left) + appendPQExpBufferChar(&left_literal, '"'); + cp++; + } + else + inquotes = !inquotes; + cp++; + } + else if (!inquotes && isupper((unsigned char) ch)) + { + appendPQExpBufferChar(curbuf, + pg_tolower((unsigned char) ch)); + if (left) + appendPQExpBufferChar(&left_literal, + pg_tolower((unsigned char) ch)); + cp++; + } + else if (!inquotes && ch == '*') + { + appendPQExpBufferStr(curbuf, ".*"); + if (left) + appendPQExpBufferChar(&left_literal, '*'); + cp++; + } + else if (!inquotes && ch == '?') + { + appendPQExpBufferChar(curbuf, '.'); + if (left) + appendPQExpBufferChar(&left_literal, '?'); + cp++; + } + else if (!inquotes && ch == '.') + { + left = false; + if (dotcnt) + (*dotcnt)++; + if (curbuf < maxbuf) + { + appendPQExpBufferStr(curbuf, ")$"); + curbuf++; + initPQExpBuffer(curbuf); + appendPQExpBufferStr(curbuf, "^("); + cp++; + } + else + appendPQExpBufferChar(curbuf, *cp++); + } + else if (ch == '$') + { + /* + * Dollar is always quoted, whether inside quotes or not. The + * reason is that it's allowed in SQL identifiers, so there's a + * significant use-case for treating it literally, while because + * we anchor the pattern automatically there is no use-case for + * having it possess its regexp meaning. + */ + appendPQExpBufferStr(curbuf, "\\$"); + if (left) + appendPQExpBufferChar(&left_literal, '$'); + cp++; + } + else + { + /* + * Ordinary data character, transfer to pattern + * + * Inside double quotes, or at all times if force_escape is true, + * quote regexp special characters with a backslash to avoid + * regexp errors. Outside quotes, however, let them pass through + * as-is; this lets knowledgeable users build regexp expressions + * that are more powerful than shell-style patterns. + * + * As an exception to that, though, always quote "[]", as that's + * much more likely to be an attempt to write an array type name + * than it is to be the start of a regexp bracket expression. + */ + if ((inquotes || force_escape) && + strchr("|*+?()[]{}.^$\\", ch)) + appendPQExpBufferChar(curbuf, '\\'); + else if (ch == '[' && cp[1] == ']') + appendPQExpBufferChar(curbuf, '\\'); + i = PQmblenBounded(cp, encoding); + while (i--) + { + if (left) + appendPQExpBufferChar(&left_literal, *cp); + appendPQExpBufferChar(curbuf, *cp++); + } + } + } + appendPQExpBufferStr(curbuf, ")$"); + + if (namebuf) + { + appendPQExpBufferStr(namebuf, curbuf->data); + termPQExpBuffer(curbuf); + curbuf--; + } + + if (schemabuf && curbuf >= buf) + { + appendPQExpBufferStr(schemabuf, curbuf->data); + termPQExpBuffer(curbuf); + curbuf--; + } + + if (dbnamebuf && curbuf >= buf) + { + if (want_literal_dbname) + appendPQExpBufferStr(dbnamebuf, left_literal.data); + else + appendPQExpBufferStr(dbnamebuf, curbuf->data); + termPQExpBuffer(curbuf); + } + + if (want_literal_dbname) + termPQExpBuffer(&left_literal); +} -- cgit v1.2.3