On 04.01.23 01:37, Nathan Bossart wrote:
In general, +1.
On Tue, Dec 20, 2022 at 06:30:40AM +0100, Peter Eisentraut wrote:
(Still need to think about Robert's comment about lack of error context.)
Would adding the name of the GUC be sufficient?
ereport(ERROR,
(errmsg("could not build %s", guc_name),
errdetail("string ends unexpectedly after escape character
\"%%\"")));
done
The errors now basically look like an invalid GUC value.
+ * A value may be NULL. If the corresponding placeholder is found in the
+ * input string, the whole function returns NULL.
This appears to be carried over from BuildRestoreCommand(), and AFAICT it
is only necessary because pg_rewind doesn't support %r in restore_command.
IMHO this behavior is counterintuitive and possibly error-prone and should
result in an ERROR instead. Since pg_rewind is the only special case, it
could independently check for %r before building the command.
Yeah, this annoyed me, too. I have now changed it so that a NULL
"value" is the same as an unsupported placeholder. This preserves the
existing behavior.
(Having pg_rewind check for %r itself would probably require replicating
most of the string processing logic (consider something like "%%r"), so
it didn't seem appealing.)
From 3975b1296174218c5e25e2b02bca88cb3c26ee63 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <pe...@eisentraut.org>
Date: Mon, 9 Jan 2023 09:03:30 +0100
Subject: [PATCH v4] Common function for percent placeholder replacement
There are a number of places where a shell command is constructed with
percent-placeholders (like %x). It's cumbersome to have to open-code
this several times. This factors out this logic into a separate
function. This also allows us to ensure consistency for and document
some subtle behaviors, such as what to do with unrecognized
placeholders.
Discussion:
https://www.postgresql.org/message-id/flat/5238bbed-0b01-83a6-d4b2-7eb0562a054e%40enterprisedb.com
---
.../basebackup_to_shell/basebackup_to_shell.c | 56 +------
src/backend/access/transam/xlogarchive.c | 45 +-----
src/backend/libpq/be-secure-common.c | 38 +----
src/backend/postmaster/shell_archive.c | 58 ++------
src/common/Makefile | 1 +
src/common/archive.c | 81 +----------
src/common/meson.build | 1 +
src/common/percentrepl.c | 137 ++++++++++++++++++
src/fe_utils/archive.c | 2 -
src/include/common/percentrepl.h | 18 +++
10 files changed, 190 insertions(+), 247 deletions(-)
create mode 100644 src/common/percentrepl.c
create mode 100644 src/include/common/percentrepl.h
diff --git a/contrib/basebackup_to_shell/basebackup_to_shell.c
b/contrib/basebackup_to_shell/basebackup_to_shell.c
index 8d583550b5..29f5069d42 100644
--- a/contrib/basebackup_to_shell/basebackup_to_shell.c
+++ b/contrib/basebackup_to_shell/basebackup_to_shell.c
@@ -12,6 +12,7 @@
#include "access/xact.h"
#include "backup/basebackup_target.h"
+#include "common/percentrepl.h"
#include "miscadmin.h"
#include "storage/fd.h"
#include "utils/acl.h"
@@ -208,59 +209,8 @@ static char *
shell_construct_command(const char *base_command, const char *filename,
const char *target_detail)
{
- StringInfoData buf;
- const char *c;
-
- initStringInfo(&buf);
- for (c = base_command; *c != '\0'; ++c)
- {
- /* Anything other than '%' is copied verbatim. */
- if (*c != '%')
- {
- appendStringInfoChar(&buf, *c);
- continue;
- }
-
- /* Any time we see '%' we eat the following character as well.
*/
- ++c;
-
- /*
- * The following character determines what we insert here, or
may
- * cause us to throw an error.
- */
- if (*c == '%')
- {
- /* '%%' is replaced by a single '%' */
- appendStringInfoChar(&buf, '%');
- }
- else if (*c == 'f')
- {
- /* '%f' is replaced by the filename */
- appendStringInfoString(&buf, filename);
- }
- else if (*c == 'd')
- {
- /* '%d' is replaced by the target detail */
- appendStringInfoString(&buf, target_detail);
- }
- else if (*c == '\0')
- {
- /* Incomplete escape sequence, expected a character
afterward */
- ereport(ERROR,
- errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("shell command ends unexpectedly
after escape character \"%%\""));
- }
- else
- {
- /* Unknown escape sequence */
- ereport(ERROR,
- errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("shell command contains
unexpected escape sequence \"%c\"",
- *c));
- }
- }
-
- return buf.data;
+ return replace_percent_placeholders(base_command,
"basebackup_to_shell.command",
+
"df", target_detail, filename);
}
/*
diff --git a/src/backend/access/transam/xlogarchive.c
b/src/backend/access/transam/xlogarchive.c
index 76abc74c67..f911e8c3a6 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,6 +23,7 @@
#include "access/xlog_internal.h"
#include "access/xlogarchive.h"
#include "common/archive.h"
+#include "common/percentrepl.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/startup.h"
@@ -291,11 +292,8 @@ void
ExecuteRecoveryCommand(const char *command, const char *commandName,
bool failOnSignal, uint32
wait_event_info)
{
- char xlogRecoveryCmd[MAXPGPATH];
+ char *xlogRecoveryCmd;
char lastRestartPointFname[MAXPGPATH];
- char *dp;
- char *endp;
- const char *sp;
int rc;
XLogSegNo restartSegNo;
XLogRecPtr restartRedoPtr;
@@ -316,42 +314,7 @@ ExecuteRecoveryCommand(const char *command, const char
*commandName,
/*
* construct the command to be executed
*/
- dp = xlogRecoveryCmd;
- endp = xlogRecoveryCmd + MAXPGPATH - 1;
- *endp = '\0';
-
- for (sp = command; *sp; sp++)
- {
- if (*sp == '%')
- {
- switch (sp[1])
- {
- case 'r':
- /* %r: filename of last restartpoint */
- sp++;
- strlcpy(dp, lastRestartPointFname, endp
- dp);
- dp += strlen(dp);
- break;
- case '%':
- /* convert %% to a single % */
- sp++;
- if (dp < endp)
- *dp++ = *sp;
- break;
- default:
- /* otherwise treat the % as not special
*/
- if (dp < endp)
- *dp++ = *sp;
- break;
- }
- }
- else
- {
- if (dp < endp)
- *dp++ = *sp;
- }
- }
- *dp = '\0';
+ xlogRecoveryCmd = replace_percent_placeholders(command, commandName,
"r", lastRestartPointFname);
ereport(DEBUG3,
(errmsg_internal("executing %s \"%s\"", commandName,
command)));
@@ -364,6 +327,8 @@ ExecuteRecoveryCommand(const char *command, const char
*commandName,
rc = system(xlogRecoveryCmd);
pgstat_report_wait_end();
+ pfree(xlogRecoveryCmd);
+
if (rc != 0)
{
/*
diff --git a/src/backend/libpq/be-secure-common.c
b/src/backend/libpq/be-secure-common.c
index f6078c91c3..ab5e2dfa2b 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -22,6 +22,7 @@
#include <sys/stat.h>
#include <unistd.h>
+#include "common/percentrepl.h"
#include "common/string.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
@@ -39,8 +40,7 @@ int
run_ssl_passphrase_command(const char *prompt, bool is_server_start, char
*buf, int size)
{
int loglevel = is_server_start ? ERROR : LOG;
- StringInfoData command;
- char *p;
+ char *command;
FILE *fh;
int pclose_rc;
size_t len = 0;
@@ -49,37 +49,15 @@ run_ssl_passphrase_command(const char *prompt, bool
is_server_start, char *buf,
Assert(size > 0);
buf[0] = '\0';
- initStringInfo(&command);
+ command = replace_percent_placeholders(ssl_passphrase_command,
"ssl_passphrase_command", "p", prompt);
- for (p = ssl_passphrase_command; *p; p++)
- {
- if (p[0] == '%')
- {
- switch (p[1])
- {
- case 'p':
- appendStringInfoString(&command,
prompt);
- p++;
- break;
- case '%':
- appendStringInfoChar(&command, '%');
- p++;
- break;
- default:
- appendStringInfoChar(&command, p[0]);
- }
- }
- else
- appendStringInfoChar(&command, p[0]);
- }
-
- fh = OpenPipeStream(command.data, "r");
+ fh = OpenPipeStream(command, "r");
if (fh == NULL)
{
ereport(loglevel,
(errcode_for_file_access(),
errmsg("could not execute command \"%s\": %m",
- command.data)));
+ command)));
goto error;
}
@@ -91,7 +69,7 @@ run_ssl_passphrase_command(const char *prompt, bool
is_server_start, char *buf,
ereport(loglevel,
(errcode_for_file_access(),
errmsg("could not read from command
\"%s\": %m",
- command.data)));
+ command)));
goto error;
}
}
@@ -111,7 +89,7 @@ run_ssl_passphrase_command(const char *prompt, bool
is_server_start, char *buf,
ereport(loglevel,
(errcode_for_file_access(),
errmsg("command \"%s\" failed",
- command.data),
+ command),
errdetail_internal("%s",
wait_result_to_str(pclose_rc))));
goto error;
}
@@ -120,7 +98,7 @@ run_ssl_passphrase_command(const char *prompt, bool
is_server_start, char *buf,
len = pg_strip_crlf(buf);
error:
- pfree(command.data);
+ pfree(command);
return len;
}
diff --git a/src/backend/postmaster/shell_archive.c
b/src/backend/postmaster/shell_archive.c
index 6f98414a40..5d97c6df64 100644
--- a/src/backend/postmaster/shell_archive.c
+++ b/src/backend/postmaster/shell_archive.c
@@ -18,6 +18,7 @@
#include <sys/wait.h>
#include "access/xlog.h"
+#include "common/percentrepl.h"
#include "pgstat.h"
#include "postmaster/pgarch.h"
@@ -44,58 +45,17 @@ shell_archive_configured(void)
static bool
shell_archive_file(const char *file, const char *path)
{
- char xlogarchcmd[MAXPGPATH];
- char *dp;
- char *endp;
- const char *sp;
+ char *xlogarchcmd;
+ char *nativePath = NULL;
int rc;
- /*
- * construct the command to be executed
- */
- dp = xlogarchcmd;
- endp = xlogarchcmd + MAXPGPATH - 1;
- *endp = '\0';
-
- for (sp = XLogArchiveCommand; *sp; sp++)
+ if (path)
{
- if (*sp == '%')
- {
- switch (sp[1])
- {
- case 'p':
- /* %p: relative path of source file */
- sp++;
- strlcpy(dp, path, endp - dp);
- make_native_path(dp);
- dp += strlen(dp);
- break;
- case 'f':
- /* %f: filename of source file */
- sp++;
- strlcpy(dp, file, endp - dp);
- dp += strlen(dp);
- break;
- case '%':
- /* convert %% to a single % */
- sp++;
- if (dp < endp)
- *dp++ = *sp;
- break;
- default:
- /* otherwise treat the % as not special
*/
- if (dp < endp)
- *dp++ = *sp;
- break;
- }
- }
- else
- {
- if (dp < endp)
- *dp++ = *sp;
- }
+ nativePath = pstrdup(path);
+ make_native_path(nativePath);
}
- *dp = '\0';
+
+ xlogarchcmd = replace_percent_placeholders(XLogArchiveCommand,
"archive_command", "fp", file, nativePath);
ereport(DEBUG3,
(errmsg_internal("executing archive command \"%s\"",
@@ -155,6 +115,8 @@ shell_archive_file(const char *file, const char *path)
return false;
}
+ pfree(xlogarchcmd);
+
elog(DEBUG1, "archived write-ahead log file \"%s\"", file);
return true;
}
diff --git a/src/common/Makefile b/src/common/Makefile
index 898701fae1..113029bf7b 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -65,6 +65,7 @@ OBJS_COMMON = \
kwlookup.o \
link-canary.o \
md5_common.o \
+ percentrepl.o \
pg_get_line.o \
pg_lzcompress.o \
pg_prng.o \
diff --git a/src/common/archive.c b/src/common/archive.c
index 1466f67ea6..fb95006671 100644
--- a/src/common/archive.c
+++ b/src/common/archive.c
@@ -20,7 +20,7 @@
#endif
#include "common/archive.h"
-#include "lib/stringinfo.h"
+#include "common/percentrepl.h"
/*
* BuildRestoreCommand
@@ -41,81 +41,14 @@ BuildRestoreCommand(const char *restoreCommand,
const char *xlogfname,
const char *lastRestartPointFname)
{
- StringInfoData result;
- const char *sp;
+ char *nativePath = NULL;
- /*
- * Build the command to be executed.
- */
- initStringInfo(&result);
-
- for (sp = restoreCommand; *sp; sp++)
+ if (xlogpath)
{
- if (*sp == '%')
- {
- switch (sp[1])
- {
- case 'p':
- {
- char *nativePath;
-
- /* %p: relative path of target
file */
- if (xlogpath == NULL)
- {
- pfree(result.data);
- return NULL;
- }
- sp++;
-
- /*
- * This needs to use a
placeholder to not modify the
- * input with the conversion
done via
- * make_native_path().
- */
- nativePath = pstrdup(xlogpath);
- make_native_path(nativePath);
- appendStringInfoString(&result,
-
nativePath);
- pfree(nativePath);
- break;
- }
- case 'f':
- /* %f: filename of desired file */
- if (xlogfname == NULL)
- {
- pfree(result.data);
- return NULL;
- }
- sp++;
- appendStringInfoString(&result,
xlogfname);
- break;
- case 'r':
- /* %r: filename of last restartpoint */
- if (lastRestartPointFname == NULL)
- {
- pfree(result.data);
- return NULL;
- }
- sp++;
- appendStringInfoString(&result,
-
lastRestartPointFname);
- break;
- case '%':
- /* convert %% to a single % */
- sp++;
- appendStringInfoChar(&result, *sp);
- break;
- default:
- /* otherwise treat the % as not special
*/
- appendStringInfoChar(&result, *sp);
- break;
- }
- }
- else
- {
- appendStringInfoChar(&result, *sp);
- }
+ nativePath = pstrdup(xlogpath);
+ make_native_path(nativePath);
}
- return result.data;
+ return replace_percent_placeholders(restoreCommand, "restore_command",
"frp",
+
xlogfname, lastRestartPointFname, nativePath);
}
diff --git a/src/common/meson.build b/src/common/meson.build
index a1fc398d8e..41bd58ebdf 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -17,6 +17,7 @@ common_sources = files(
'kwlookup.c',
'link-canary.c',
'md5_common.c',
+ 'percentrepl.c',
'pg_get_line.c',
'pg_lzcompress.c',
'pg_prng.c',
diff --git a/src/common/percentrepl.c b/src/common/percentrepl.c
new file mode 100644
index 0000000000..25cfd0b2d4
--- /dev/null
+++ b/src/common/percentrepl.c
@@ -0,0 +1,137 @@
+/*-------------------------------------------------------------------------
+ *
+ * percentrepl.c
+ * Common routines to replace percent placeholders in strings
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/common/percentrepl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#include "common/logging.h"
+#endif
+
+#include "common/percentrepl.h"
+#include "lib/stringinfo.h"
+
+/*
+ * replace_percent_placeholders
+ *
+ * Replace percent-letter placeholders in input string with the supplied
+ * values. For example, to replace %f with foo and %b with bar, call
+ *
+ * replace_percent_placeholders(instr, "bf", bar, foo);
+ *
+ * The return value is palloc'd.
+ *
+ * "%%" is replaced by a single "%".
+ *
+ * This throws an error for an unsupported placeholder or a "%" at the end of
+ * the input string.
+ *
+ * A value may be NULL. If the corresponding placeholder is found in the
+ * input string, it will be treated as if an unsupported placeholder was used.
+ * This allows callers to share a "letters" specification but vary the
+ * actually supported placeholders at run time.
+ *
+ * This functions is meant for cases where all the values are readily
+ * available or cheap to compute and most invocations will use most values
+ * (for example for archive_command). Also, it requires that all values are
+ * strings. It won't be a good match for things like log prefixes or prompts
+ * that use a mix of data types and any invocation will only use a few of the
+ * possible values.
+ *
+ * param_name is the name of the underlying GUC parameter, for error
+ * reporting. At the moment, this function is only used for GUC parameters.
+ * If other kinds of uses were added, the error reporting would need to be
+ * revised.
+ */
+char *
+replace_percent_placeholders(const char *instr, const char *param_name, const
char *letters, ...)
+{
+ StringInfoData result;
+
+ initStringInfo(&result);
+
+ for (const char *sp = instr; *sp; sp++)
+ {
+ if (*sp == '%')
+ {
+ if (sp[1] == '%')
+ {
+ /* Convert %% to a single % */
+ sp++;
+ appendStringInfoChar(&result, *sp);
+ }
+ else if (sp[1] == '\0')
+ {
+ /* Incomplete escape sequence, expected a
character afterward */
+#ifdef FRONTEND
+ pg_log_error("invalid value for parameter
\"%s\": \"%s\"", param_name, instr);
+ pg_log_error_detail("String ends unexpectedly
after escape character \"%%\".");
+ exit(1);
+#else
+ ereport(ERROR,
+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid value for
parameter \"%s\": \"%s\"", param_name, instr),
+ errdetail("String ends
unexpectedly after escape character \"%%\"."));
+#endif
+ }
+ else
+ {
+ /* Look up placeholder character */
+ bool found = false;
+ va_list ap;
+
+ sp++;
+
+ va_start(ap, letters);
+ for (const char *lp = letters; *lp; lp++)
+ {
+ char *val = va_arg(ap, char *);
+
+ if (*sp == *lp)
+ {
+ if (val)
+ {
+
appendStringInfoString(&result, val);
+ found = true;
+ }
+ /* If val is NULL, we will
report an error. */
+ break;
+ }
+ }
+ va_end(ap);
+ if (!found)
+ {
+ /* Unknown escape sequence */
+#ifdef FRONTEND
+ pg_log_error("invalid value for
parameter \"%s\": \"%s\"", param_name, instr);
+ pg_log_error_detail("String contains
unexpected escape sequence \"%c\".", *sp);
+ exit(1);
+#else
+ ereport(ERROR,
+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid value
for parameter \"%s\": \"%s\"", param_name, instr),
+ errdetail("String
contains unexpected escape sequence \"%c\".", *sp));
+#endif
+ }
+ }
+ }
+ else
+ {
+ appendStringInfoChar(&result, *sp);
+ }
+ }
+
+ return result.data;
+}
diff --git a/src/fe_utils/archive.c b/src/fe_utils/archive.c
index aab813c102..eb1c930ae7 100644
--- a/src/fe_utils/archive.c
+++ b/src/fe_utils/archive.c
@@ -48,8 +48,6 @@ RestoreArchivedFile(const char *path, const char *xlogfname,
xlogRestoreCmd = BuildRestoreCommand(restoreCommand, xlogpath,
xlogfname, NULL);
- if (xlogRestoreCmd == NULL)
- pg_fatal("cannot use restore_command with %%r placeholder");
/*
* Execute restore_command, which should copy the missing file from
diff --git a/src/include/common/percentrepl.h b/src/include/common/percentrepl.h
new file mode 100644
index 0000000000..2dd88db144
--- /dev/null
+++ b/src/include/common/percentrepl.h
@@ -0,0 +1,18 @@
+/*-------------------------------------------------------------------------
+ *
+ * percentrepl.h
+ * Common routines to replace percent placeholders in strings
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/percentrepl.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PERCENTREPL_H
+#define PERCENTREPL_H
+
+extern char *replace_percent_placeholders(const char *instr, const char
*param_name, const char *letters, ...);
+
+#endif /* PERCENTREPL_H */
base-commit: 3c569049b7b502bb4952483d19ce622ff0af5fd6
--
2.39.0