libpq: Add "servicefile" connection option
authorMichael Paquier <[email protected]>
Sun, 13 Jul 2025 07:52:19 +0000 (16:52 +0900)
committerMichael Paquier <[email protected]>
Sun, 13 Jul 2025 07:52:19 +0000 (16:52 +0900)
This commit adds the possibility to specify a service file in a
connection string, using a new option called "servicefile".  The parsing
of the service file happens so as things are done in this order of
priority:
- The servicefile connection option.
- Environment variable PGSERVICEFILE.
- Default path, depending on the HOME environment.

Note that in the last default case, we need to fill in "servicefile" for
the connection's PQconninfoOption to let clients know which service file
has been used for the connection.  Some TAP tests are added, with a few
tweaks required for Windows when using URIs or connection option values,
for the location paths.

Author: Torsten Förtsch <[email protected]>
Co-authored-by: Ryo Kanbayashi <[email protected]>
Discussion: https://postgr.es/m/CAKkG4_nCjx3a_F3gyXHSPWxD8Sd8URaM89wey7fG_9g7KBkOCQ@mail.gmail.com

doc/src/sgml/libpq.sgml
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/libpq-int.h
src/interfaces/libpq/t/006_service.pl

index b2c2cf9eac831e0297ec9db6a8cb72799db35ca1..5bf59a19855594efc4e119d515692ab719a36422 100644 (file)
@@ -2320,6 +2320,19 @@ 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 name of the per-user connection service file
+        (see <xref linkend="libpq-pgservice"/>).
+        Defaults to <filename>~/.pg_service.conf</filename>, or
+        <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
+        Microsoft Windows.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-target-session-attrs" xreflabel="target_session_attrs">
       <term><literal>target_session_attrs</literal></term>
       <listitem>
@@ -9140,12 +9153,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       <indexterm>
        <primary><envar>PGSERVICEFILE</envar></primary>
       </indexterm>
-      <envar>PGSERVICEFILE</envar> specifies the name of the per-user
-      connection service file
-      (see <xref linkend="libpq-pgservice"/>).
-      Defaults to <filename>~/.pg_service.conf</filename>, or
-      <filename>%APPDATA%\postgresql\.pg_service.conf</filename> on
-      Microsoft Windows.
+      <envar>PGSERVICEFILE</envar> behaves the same as the
+      <xref linkend="libpq-connect-servicefile"/> connection parameter.
      </para>
     </listitem>
 
@@ -9576,7 +9585,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
index 09eb79812ac6d71556f23cc37f3121d51cf219fa..2a2b10d5a29baa770fdd60b5b2db65e35ba96d69 100644 (file)
@@ -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,10 +5939,13 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
        return 0;
 
    /*
-    * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that
-    * exists).
+    * First, try the "servicefile" option in connection string.  Then, try
+    * the 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
    {
@@ -6092,7 +6101,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;
@@ -6135,6 +6154,33 @@ parseServiceFile(const char *serviceFile,
    }
 
 exit:
+
+   /*
+    * If a service has been successfully found, set the "servicefile" option
+    * if not already set.  This matters when we use a default service file or
+    * PGSERVICEFILE, where we want to be able track the value.
+    */
+   if (*group_found && result == 0)
+   {
+       for (i = 0; options[i].keyword; i++)
+       {
+           if (strcmp(options[i].keyword, "servicefile") != 0)
+               continue;
+
+           /* If value is already set, nothing to do */
+           if (options[i].val != NULL)
+               break;
+
+           options[i].val = strdup(serviceFile);
+           if (options[i].val == NULL)
+           {
+               libpq_append_error(errorMessage, "out of memory");
+               result = 3;
+           }
+           break;
+       }
+   }
+
    fclose(f);
 
    return result;
index a6cfd7f5c9d83b89a0405f43d867e3f04f229f31..70c28f2ffca0b9bc9e3c651b7c671d81761980d6 100644 (file)
@@ -389,6 +389,8 @@ 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) */
index d896558a6cc248f65e13647ebf94af153b2c4566..797e6232b8fcbc226c3a9ca769339b986f94ca25 100644 (file)
@@ -53,6 +53,13 @@ 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 +165,77 @@ 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/);
+
+   local $ENV{PGSERVICEFILE} = $srvfile_nested_2;
+
+   $dummy_node->connect_fails(
+       'service=my_srv',
+       'connection with "servicefile" in nested service file',
        expected_stderr =>
-         qr/nested service specifications not supported in service file/);
+         qr/nested "servicefile" specifications not supported in service file/
+   );
+}
+
+# Properly escape backslashes in the path, to ensure the generation of
+# correct connection strings.
+my $srvfile_win_cared = $srvfile_valid;
+$srvfile_win_cared =~ s/\\/\\\\/g;
+
+# Checks that the "servicefile" option works as expected
+{
+   $dummy_node->connect_ok(
+       q{service=my_srv servicefile='} . $srvfile_win_cared . q{'},
+       'connection with valid servicefile in connection string',
+       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,
+       'connection with valid servicefile in URI',
+       sql => "SELECT 'connect3_2'",
+       expected_stdout => qr/connect3_2/);
+
+   local $ENV{PGSERVICE} = 'my_srv';
+   $dummy_node->connect_ok(
+       q{servicefile='} . $srvfile_win_cared . q{'},
+       'connection with PGSERVICE and servicefile in connection string',
+       sql => "SELECT 'connect3_3'",
+       expected_stdout => qr/connect3_3/);
+
+   $dummy_node->connect_ok(
+       'postgresql://?servicefile=' . $encoded_srvfile,
+       'connection with PGSERVICE and servicefile in URI',
+       sql => "SELECT 'connect3_4'",
+       expected_stdout => qr/connect3_4/);
+}
+
+# Check that the "servicefile" option takes priority over the PGSERVICEFILE
+# environment variable.
+{
+   local $ENV{PGSERVICEFILE} = 'non-existent-file.conf';
+
+   $dummy_node->connect_fails(
+       'service=my_srv',
+       'connection with invalid 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{'},
+       'connection with both servicefile and PGSERVICEFILE',
+       sql => "SELECT 'connect4_1'",
+       expected_stdout => qr/connect4_1/);
 }
 
 $node->teardown_node;