From 19d59646e1842ffd73aa0d79fc80c89370878f60 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 9 Jul 2025 16:17:36 +0900
Subject: [PATCH v12 1/2] servicefile option usage on connection string feature
 and its tests.

---
 src/interfaces/libpq/fe-connect.c     | 31 ++++++++--
 src/interfaces/libpq/libpq-int.h      |  1 +
 src/interfaces/libpq/t/006_service.pl | 81 ++++++++++++++++++++++++++-
 doc/src/sgml/libpq.sgml               | 15 ++++-
 4 files changed, 121 insertions(+), 7 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 09eb79812ac6..f9d626d9991c 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Service", "", 20,
 	offsetof(struct pg_conn, pgservice)},
 
+	{"servicefile", "PGSERVICEFILE", NULL, NULL,
+	"Database-Service-File", "", 64,
+	offsetof(struct pg_conn, pgservicefile)},
+
 	{"user", "PGUSER", NULL, NULL,
 		"Database-User", "", 20,
 	offsetof(struct pg_conn, pguser)},
@@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn)
 	free(conn->dbName);
 	free(conn->replication);
 	free(conn->pgservice);
+	free(conn->pgservicefile);
 	free(conn->pguser);
 	if (conn->pgpass)
 	{
@@ -5914,6 +5919,7 @@ static int
 parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
+	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		serviceFile[MAXPGPATH];
 	char	   *env;
 	bool		group_found = false;
@@ -5933,11 +5939,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 		return 0;
 
 	/*
-	 * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-	 * exists).
+	 * First, check servicefile option on connection string. Second, check
+	 * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf
+	 * (if that exists).
 	 */
-	if ((env = getenv("PGSERVICEFILE")) != NULL)
+	if (service_fname != NULL)
+	{
+		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+	}
+	else if ((env = getenv("PGSERVICEFILE")) != NULL)
+	{
 		strlcpy(serviceFile, env, sizeof(serviceFile));
+	}
 	else
 	{
 		char		homedir[MAXPGPATH];
@@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile,
 				if (strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
-									   "nested service specifications not supported in service file \"%s\", line %d",
+									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
+									   serviceFile,
+									   linenr);
+					result = 3;
+					goto exit;
+				}
+
+				if (strcmp(key, "servicefile") == 0)
+				{
+					libpq_append_error(errorMessage,
+									   "nested \"servicefile\" specifications not supported in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index a6cfd7f5c9d8..5ae4e88f0b75 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -389,6 +389,7 @@ struct pg_conn
 	char	   *dbName;			/* database name */
 	char	   *replication;	/* connect as the replication standby? */
 	char	   *pgservice;		/* Postgres service, if any */
+	char	   *pgservicefile;	/* path to a service file containing service(s) */
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index d896558a6cc2..d10cc206c468 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -53,6 +53,12 @@ copy($srvfile_valid, $srvfile_nested)
   or die "Could not copy $srvfile_valid to $srvfile_nested: $!";
 append_to_file($srvfile_nested, 'service=invalid_srv' . $newline);
 
+# Service file with nested "servicefile" defined.
+my $srvfile_nested_2 = "$td/pg_service_nested_2.conf";
+copy($srvfile_valid, $srvfile_nested_2) or
+   die "Could not copy $srvfile_valid to $srvfile_nested_2: $!";
+append_to_file($srvfile_nested_2, 'servicefile=' . $srvfile_default . $newline);
+
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
 # defined in PGSERVICEFILE cannot be found, or when a service file is
@@ -158,9 +164,80 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 
 	$dummy_node->connect_fails(
 		'service=my_srv',
-		'connection with nested service file',
+		'connection with "service" in nested service file',
 		expected_stderr =>
-		  qr/nested service specifications not supported in service file/);
+		  qr/nested "service" specifications not supported in service file/);
+
+	local $ENV{PGSERVICEFILE} = $srvfile_nested_2;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'connection with "servicefile" in nested service file',
+		expected_stderr => qr/nested "servicefile" specifications not supported in service file/
+	);
+}
+
+# Use correct escaped path for Windows.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Check that "servicefile" option works as expected
+{
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'service=my_srv servicefile=...',
+		sql             => "SELECT 'connect3_1'",
+		expected_stdout => qr/connect3_1/
+	);
+
+	# Encode slashes and backslash
+	my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{
+		$1 eq '/' ? '%2F' : '%5C'
+	}ger;
+
+	# Additionally encode a colon in servicefile path of Windows
+	$encoded_srvfile =~ s/:/%3A/g;
+
+	$dummy_node->connect_ok(
+		'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile,
+		'postgresql:///?service=my_srv&servicefile=...',
+		sql             => "SELECT 'connect3_2'",
+		expected_stdout => qr/connect3_2/
+	);
+
+	local $ENV{PGSERVICE} = 'my_srv';
+	$dummy_node->connect_ok(
+		q{servicefile='} . $srvfile_win_cared . q{'},
+		'envvar: PGSERVICE=my_srv + servicefile=...',
+		sql             => "SELECT 'connect3_3'",
+		expected_stdout => qr/connect3_3/
+	);
+
+	$dummy_node->connect_ok(
+		'postgresql://?servicefile=' . $encoded_srvfile,
+		'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...',
+		sql             => "SELECT 'connect3_4'",
+		expected_stdout => qr/connect3_4/
+	);
+}
+
+# Check that "servicefile" option takes precedence over PGSERVICEFILE
+# environment variable
+{
+	local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'service=... fails with wrong PGSERVICEFILE',
+		expected_stderr => qr/service file "non-existent-file\.conf" not found/
+	);
+
+	$dummy_node->connect_ok(
+		q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+		'servicefile= takes precedence over PGSERVICEFILE',
+		sql             => "SELECT 'connect4_1'",
+		expected_stdout => qr/connect4_1/
+	);
 }
 
 $node->teardown_node;
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b2c2cf9eac83..c4c4bb35ac29 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2320,6 +2320,16 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-servicefile" xreflabel="servicefile">
+      <term><literal>servicefile</literal></term>
+      <listitem>
+       <para>
+        This option specifies the location of connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9146,6 +9156,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       Defaults to <filename>~/.pg_service.conf</filename>, or
       <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
       Microsoft Windows.
+      <envar>This environment variable</envar> behaves the same as the <xref
+      linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9576,7 +9588,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    On Microsoft Windows, it is named
    <filename>%APPDATA%\postgresql\.pg_service.conf</filename> (where
    <filename>%APPDATA%</filename> refers to the Application Data subdirectory
-   in the user's profile).  A different file name can be specified by
+   in the user's profile).  A different file name can be specified using the
+   <literal>servicefile</literal> key word in a libpq connection string or by
    setting the environment variable <envar>PGSERVICEFILE</envar>.
    The system-wide file is named <filename>pg_service.conf</filename>.
    By default it is sought in the <filename>etc</filename> directory
-- 
2.50.0

