LCOV - code coverage report
Current view: top level - contrib/adminpack - adminpack.c (source / functions) Hit Total Coverage
Test: PostgreSQL 16beta1 Lines: 79 173 45.7 %
Date: 2023-06-02 18:12:27 Functions: 17 24 70.8 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*-------------------------------------------------------------------------
       2             :  *
       3             :  * adminpack.c
       4             :  *
       5             :  *
       6             :  * Copyright (c) 2002-2023, PostgreSQL Global Development Group
       7             :  *
       8             :  * Author: Andreas Pflug <pgadmin@pse-consulting.de>
       9             :  *
      10             :  * IDENTIFICATION
      11             :  *    contrib/adminpack/adminpack.c
      12             :  *
      13             :  *-------------------------------------------------------------------------
      14             :  */
      15             : #include "postgres.h"
      16             : 
      17             : #include <sys/file.h>
      18             : #include <sys/stat.h>
      19             : #include <unistd.h>
      20             : 
      21             : #include "catalog/pg_authid.h"
      22             : #include "catalog/pg_type.h"
      23             : #include "funcapi.h"
      24             : #include "miscadmin.h"
      25             : #include "postmaster/syslogger.h"
      26             : #include "storage/fd.h"
      27             : #include "utils/acl.h"
      28             : #include "utils/builtins.h"
      29             : #include "utils/datetime.h"
      30             : 
      31             : 
      32             : #ifdef WIN32
      33             : 
      34             : #ifdef rename
      35             : #undef rename
      36             : #endif
      37             : 
      38             : #ifdef unlink
      39             : #undef unlink
      40             : #endif
      41             : #endif
      42             : 
      43           2 : PG_MODULE_MAGIC;
      44             : 
      45           2 : PG_FUNCTION_INFO_V1(pg_file_write);
      46           8 : PG_FUNCTION_INFO_V1(pg_file_write_v1_1);
      47           4 : PG_FUNCTION_INFO_V1(pg_file_sync);
      48           2 : PG_FUNCTION_INFO_V1(pg_file_rename);
      49           4 : PG_FUNCTION_INFO_V1(pg_file_rename_v1_1);
      50           2 : PG_FUNCTION_INFO_V1(pg_file_unlink);
      51           4 : PG_FUNCTION_INFO_V1(pg_file_unlink_v1_1);
      52           2 : PG_FUNCTION_INFO_V1(pg_logdir_ls);
      53           2 : PG_FUNCTION_INFO_V1(pg_logdir_ls_v1_1);
      54             : 
      55             : static int64 pg_file_write_internal(text *file, text *data, bool replace);
      56             : static bool pg_file_rename_internal(text *file1, text *file2, text *file3);
      57             : static Datum pg_logdir_ls_internal(FunctionCallInfo fcinfo);
      58             : 
      59             : 
      60             : /*-----------------------
      61             :  * some helper functions
      62             :  */
      63             : 
      64             : /*
      65             :  * Convert a "text" filename argument to C string, and check it's allowable.
      66             :  *
      67             :  * Filename may be absolute or relative to the DataDir, but we only allow
      68             :  * absolute paths that match DataDir.
      69             :  */
      70             : static char *
      71          46 : convert_and_check_filename(text *arg)
      72             : {
      73          46 :     char       *filename = text_to_cstring(arg);
      74             : 
      75          46 :     canonicalize_path(filename);    /* filename can change length here */
      76             : 
      77             :     /*
      78             :      * Members of the 'pg_write_server_files' role are allowed to access any
      79             :      * files on the server as the PG user, so no need to do any further checks
      80             :      * here.
      81             :      */
      82          46 :     if (has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES))
      83          38 :         return filename;
      84             : 
      85             :     /*
      86             :      * User isn't a member of the pg_write_server_files role, so check if it's
      87             :      * allowable
      88             :      */
      89           8 :     if (is_absolute_path(filename))
      90             :     {
      91             :         /* Allow absolute paths if within DataDir */
      92           6 :         if (!path_is_prefix_of_path(DataDir, filename))
      93           4 :             ereport(ERROR,
      94             :                     (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
      95             :                      errmsg("absolute path not allowed")));
      96             :     }
      97           2 :     else if (!path_is_relative_and_below_cwd(filename))
      98           2 :         ereport(ERROR,
      99             :                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     100             :                  errmsg("path must be in or below the data directory")));
     101             : 
     102           2 :     return filename;
     103             : }
     104             : 
     105             : 
     106             : /*
     107             :  * check for superuser, bark if not.
     108             :  */
     109             : static void
     110           0 : requireSuperuser(void)
     111             : {
     112           0 :     if (!superuser())
     113           0 :         ereport(ERROR,
     114             :                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     115             :                  errmsg("only superuser may access generic file functions")));
     116           0 : }
     117             : 
     118             : 
     119             : 
     120             : /* ------------------------------------
     121             :  * pg_file_write - old version
     122             :  *
     123             :  * The superuser() check here must be kept as the library might be upgraded
     124             :  * without the extension being upgraded, meaning that in pre-1.1 installations
     125             :  * these functions could be called by any user.
     126             :  */
     127             : Datum
     128           0 : pg_file_write(PG_FUNCTION_ARGS)
     129             : {
     130           0 :     text       *file = PG_GETARG_TEXT_PP(0);
     131           0 :     text       *data = PG_GETARG_TEXT_PP(1);
     132           0 :     bool        replace = PG_GETARG_BOOL(2);
     133           0 :     int64       count = 0;
     134             : 
     135           0 :     requireSuperuser();
     136             : 
     137           0 :     count = pg_file_write_internal(file, data, replace);
     138             : 
     139           0 :     PG_RETURN_INT64(count);
     140             : }
     141             : 
     142             : /* ------------------------------------
     143             :  * pg_file_write_v1_1 - Version 1.1
     144             :  *
     145             :  * As of adminpack version 1.1, we no longer need to check if the user
     146             :  * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
     147             :  * Users can then grant access to it based on their policies.
     148             :  *
     149             :  * Otherwise identical to pg_file_write (above).
     150             :  */
     151             : Datum
     152          16 : pg_file_write_v1_1(PG_FUNCTION_ARGS)
     153             : {
     154          16 :     text       *file = PG_GETARG_TEXT_PP(0);
     155          16 :     text       *data = PG_GETARG_TEXT_PP(1);
     156          16 :     bool        replace = PG_GETARG_BOOL(2);
     157          16 :     int64       count = 0;
     158             : 
     159          16 :     count = pg_file_write_internal(file, data, replace);
     160             : 
     161           8 :     PG_RETURN_INT64(count);
     162             : }
     163             : 
     164             : /* ------------------------------------
     165             :  * pg_file_write_internal - Workhorse for pg_file_write functions.
     166             :  *
     167             :  * This handles the actual work for pg_file_write.
     168             :  */
     169             : static int64
     170          16 : pg_file_write_internal(text *file, text *data, bool replace)
     171             : {
     172             :     FILE       *f;
     173             :     char       *filename;
     174          16 :     int64       count = 0;
     175             : 
     176          16 :     filename = convert_and_check_filename(file);
     177             : 
     178          10 :     if (!replace)
     179             :     {
     180             :         struct stat fst;
     181             : 
     182           8 :         if (stat(filename, &fst) >= 0)
     183           2 :             ereport(ERROR,
     184             :                     (errcode(ERRCODE_DUPLICATE_FILE),
     185             :                      errmsg("file \"%s\" exists", filename)));
     186             : 
     187           6 :         f = AllocateFile(filename, "wb");
     188             :     }
     189             :     else
     190           2 :         f = AllocateFile(filename, "ab");
     191             : 
     192           8 :     if (!f)
     193           0 :         ereport(ERROR,
     194             :                 (errcode_for_file_access(),
     195             :                  errmsg("could not open file \"%s\" for writing: %m",
     196             :                         filename)));
     197             : 
     198           8 :     count = fwrite(VARDATA_ANY(data), 1, VARSIZE_ANY_EXHDR(data), f);
     199           8 :     if (count != VARSIZE_ANY_EXHDR(data) || FreeFile(f))
     200           0 :         ereport(ERROR,
     201             :                 (errcode_for_file_access(),
     202             :                  errmsg("could not write file \"%s\": %m", filename)));
     203             : 
     204           8 :     return (count);
     205             : }
     206             : 
     207             : /* ------------------------------------
     208             :  * pg_file_sync
     209             :  *
     210             :  * We REVOKE EXECUTE on the function from PUBLIC.
     211             :  * Users can then grant access to it based on their policies.
     212             :  */
     213             : Datum
     214           6 : pg_file_sync(PG_FUNCTION_ARGS)
     215             : {
     216             :     char       *filename;
     217             :     struct stat fst;
     218             : 
     219           6 :     filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));
     220             : 
     221           6 :     if (stat(filename, &fst) < 0)
     222           2 :         ereport(ERROR,
     223             :                 (errcode_for_file_access(),
     224             :                  errmsg("could not stat file \"%s\": %m", filename)));
     225             : 
     226           4 :     fsync_fname_ext(filename, S_ISDIR(fst.st_mode), false, ERROR);
     227             : 
     228           4 :     PG_RETURN_VOID();
     229             : }
     230             : 
     231             : /* ------------------------------------
     232             :  * pg_file_rename - old version
     233             :  *
     234             :  * The superuser() check here must be kept as the library might be upgraded
     235             :  * without the extension being upgraded, meaning that in pre-1.1 installations
     236             :  * these functions could be called by any user.
     237             :  */
     238             : Datum
     239           0 : pg_file_rename(PG_FUNCTION_ARGS)
     240             : {
     241             :     text       *file1;
     242             :     text       *file2;
     243             :     text       *file3;
     244             :     bool        result;
     245             : 
     246           0 :     requireSuperuser();
     247             : 
     248           0 :     if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
     249           0 :         PG_RETURN_NULL();
     250             : 
     251           0 :     file1 = PG_GETARG_TEXT_PP(0);
     252           0 :     file2 = PG_GETARG_TEXT_PP(1);
     253             : 
     254           0 :     if (PG_ARGISNULL(2))
     255           0 :         file3 = NULL;
     256             :     else
     257           0 :         file3 = PG_GETARG_TEXT_PP(2);
     258             : 
     259           0 :     result = pg_file_rename_internal(file1, file2, file3);
     260             : 
     261           0 :     PG_RETURN_BOOL(result);
     262             : }
     263             : 
     264             : /* ------------------------------------
     265             :  * pg_file_rename_v1_1 - Version 1.1
     266             :  *
     267             :  * As of adminpack version 1.1, we no longer need to check if the user
     268             :  * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
     269             :  * Users can then grant access to it based on their policies.
     270             :  *
     271             :  * Otherwise identical to pg_file_write (above).
     272             :  */
     273             : Datum
     274           6 : pg_file_rename_v1_1(PG_FUNCTION_ARGS)
     275             : {
     276             :     text       *file1;
     277             :     text       *file2;
     278             :     text       *file3;
     279             :     bool        result;
     280             : 
     281           6 :     if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
     282           0 :         PG_RETURN_NULL();
     283             : 
     284           6 :     file1 = PG_GETARG_TEXT_PP(0);
     285           6 :     file2 = PG_GETARG_TEXT_PP(1);
     286             : 
     287           6 :     if (PG_ARGISNULL(2))
     288           4 :         file3 = NULL;
     289             :     else
     290           2 :         file3 = PG_GETARG_TEXT_PP(2);
     291             : 
     292           6 :     result = pg_file_rename_internal(file1, file2, file3);
     293             : 
     294           6 :     PG_RETURN_BOOL(result);
     295             : }
     296             : 
     297             : /* ------------------------------------
     298             :  * pg_file_rename_internal - Workhorse for pg_file_rename functions.
     299             :  *
     300             :  * This handles the actual work for pg_file_rename.
     301             :  */
     302             : static bool
     303           6 : pg_file_rename_internal(text *file1, text *file2, text *file3)
     304             : {
     305             :     char       *fn1,
     306             :                *fn2,
     307             :                *fn3;
     308             :     int         rc;
     309             : 
     310           6 :     fn1 = convert_and_check_filename(file1);
     311           6 :     fn2 = convert_and_check_filename(file2);
     312             : 
     313           6 :     if (file3 == NULL)
     314           4 :         fn3 = NULL;
     315             :     else
     316           2 :         fn3 = convert_and_check_filename(file3);
     317             : 
     318           6 :     if (access(fn1, W_OK) < 0)
     319             :     {
     320           2 :         ereport(WARNING,
     321             :                 (errcode_for_file_access(),
     322             :                  errmsg("file \"%s\" is not accessible: %m", fn1)));
     323             : 
     324           2 :         return false;
     325             :     }
     326             : 
     327           4 :     if (fn3 && access(fn2, W_OK) < 0)
     328             :     {
     329           0 :         ereport(WARNING,
     330             :                 (errcode_for_file_access(),
     331             :                  errmsg("file \"%s\" is not accessible: %m", fn2)));
     332             : 
     333           0 :         return false;
     334             :     }
     335             : 
     336           4 :     rc = access(fn3 ? fn3 : fn2, W_OK);
     337           4 :     if (rc >= 0 || errno != ENOENT)
     338             :     {
     339           0 :         ereport(ERROR,
     340             :                 (errcode(ERRCODE_DUPLICATE_FILE),
     341             :                  errmsg("cannot rename to target file \"%s\"",
     342             :                         fn3 ? fn3 : fn2)));
     343             :     }
     344             : 
     345           4 :     if (fn3)
     346             :     {
     347           2 :         if (rename(fn2, fn3) != 0)
     348             :         {
     349           0 :             ereport(ERROR,
     350             :                     (errcode_for_file_access(),
     351             :                      errmsg("could not rename \"%s\" to \"%s\": %m",
     352             :                             fn2, fn3)));
     353             :         }
     354           2 :         if (rename(fn1, fn2) != 0)
     355             :         {
     356           0 :             ereport(WARNING,
     357             :                     (errcode_for_file_access(),
     358             :                      errmsg("could not rename \"%s\" to \"%s\": %m",
     359             :                             fn1, fn2)));
     360             : 
     361           0 :             if (rename(fn3, fn2) != 0)
     362             :             {
     363           0 :                 ereport(ERROR,
     364             :                         (errcode_for_file_access(),
     365             :                          errmsg("could not rename \"%s\" back to \"%s\": %m",
     366             :                                 fn3, fn2)));
     367             :             }
     368             :             else
     369             :             {
     370           0 :                 ereport(ERROR,
     371             :                         (errcode(ERRCODE_UNDEFINED_FILE),
     372             :                          errmsg("renaming \"%s\" to \"%s\" was reverted",
     373             :                                 fn2, fn3)));
     374             :             }
     375             :         }
     376             :     }
     377           2 :     else if (rename(fn1, fn2) != 0)
     378             :     {
     379           0 :         ereport(ERROR,
     380             :                 (errcode_for_file_access(),
     381             :                  errmsg("could not rename \"%s\" to \"%s\": %m", fn1, fn2)));
     382             :     }
     383             : 
     384           4 :     return true;
     385             : }
     386             : 
     387             : 
     388             : /* ------------------------------------
     389             :  * pg_file_unlink - old version
     390             :  *
     391             :  * The superuser() check here must be kept as the library might be upgraded
     392             :  * without the extension being upgraded, meaning that in pre-1.1 installations
     393             :  * these functions could be called by any user.
     394             :  */
     395             : Datum
     396           0 : pg_file_unlink(PG_FUNCTION_ARGS)
     397             : {
     398             :     char       *filename;
     399             : 
     400           0 :     requireSuperuser();
     401             : 
     402           0 :     filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));
     403             : 
     404           0 :     if (access(filename, W_OK) < 0)
     405             :     {
     406           0 :         if (errno == ENOENT)
     407           0 :             PG_RETURN_BOOL(false);
     408             :         else
     409           0 :             ereport(ERROR,
     410             :                     (errcode_for_file_access(),
     411             :                      errmsg("file \"%s\" is not accessible: %m", filename)));
     412             :     }
     413             : 
     414           0 :     if (unlink(filename) < 0)
     415             :     {
     416           0 :         ereport(WARNING,
     417             :                 (errcode_for_file_access(),
     418             :                  errmsg("could not unlink file \"%s\": %m", filename)));
     419             : 
     420           0 :         PG_RETURN_BOOL(false);
     421             :     }
     422           0 :     PG_RETURN_BOOL(true);
     423             : }
     424             : 
     425             : 
     426             : /* ------------------------------------
     427             :  * pg_file_unlink_v1_1 - Version 1.1
     428             :  *
     429             :  * As of adminpack version 1.1, we no longer need to check if the user
     430             :  * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
     431             :  * Users can then grant access to it based on their policies.
     432             :  *
     433             :  * Otherwise identical to pg_file_unlink (above).
     434             :  */
     435             : Datum
     436          10 : pg_file_unlink_v1_1(PG_FUNCTION_ARGS)
     437             : {
     438             :     char       *filename;
     439             : 
     440          10 :     filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0));
     441             : 
     442          10 :     if (access(filename, W_OK) < 0)
     443             :     {
     444           4 :         if (errno == ENOENT)
     445           4 :             PG_RETURN_BOOL(false);
     446             :         else
     447           0 :             ereport(ERROR,
     448             :                     (errcode_for_file_access(),
     449             :                      errmsg("file \"%s\" is not accessible: %m", filename)));
     450             :     }
     451             : 
     452           6 :     if (unlink(filename) < 0)
     453             :     {
     454           0 :         ereport(WARNING,
     455             :                 (errcode_for_file_access(),
     456             :                  errmsg("could not unlink file \"%s\": %m", filename)));
     457             : 
     458           0 :         PG_RETURN_BOOL(false);
     459             :     }
     460           6 :     PG_RETURN_BOOL(true);
     461             : }
     462             : 
     463             : /* ------------------------------------
     464             :  * pg_logdir_ls - Old version
     465             :  *
     466             :  * The superuser() check here must be kept as the library might be upgraded
     467             :  * without the extension being upgraded, meaning that in pre-1.1 installations
     468             :  * these functions could be called by any user.
     469             :  */
     470             : Datum
     471           0 : pg_logdir_ls(PG_FUNCTION_ARGS)
     472             : {
     473           0 :     if (!superuser())
     474           0 :         ereport(ERROR,
     475             :                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     476             :                  errmsg("only superuser can list the log directory")));
     477             : 
     478           0 :     return (pg_logdir_ls_internal(fcinfo));
     479             : }
     480             : 
     481             : /* ------------------------------------
     482             :  * pg_logdir_ls_v1_1 - Version 1.1
     483             :  *
     484             :  * As of adminpack version 1.1, we no longer need to check if the user
     485             :  * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
     486             :  * Users can then grant access to it based on their policies.
     487             :  *
     488             :  * Otherwise identical to pg_logdir_ls (above).
     489             :  */
     490             : Datum
     491           0 : pg_logdir_ls_v1_1(PG_FUNCTION_ARGS)
     492             : {
     493           0 :     return (pg_logdir_ls_internal(fcinfo));
     494             : }
     495             : 
     496             : static Datum
     497           0 : pg_logdir_ls_internal(FunctionCallInfo fcinfo)
     498             : {
     499           0 :     ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
     500             :     bool        randomAccess;
     501             :     TupleDesc   tupdesc;
     502             :     Tuplestorestate *tupstore;
     503             :     AttInMetadata *attinmeta;
     504             :     DIR        *dirdesc;
     505             :     struct dirent *de;
     506             :     MemoryContext oldcontext;
     507             : 
     508           0 :     if (strcmp(Log_filename, "postgresql-%Y-%m-%d_%H%M%S.log") != 0)
     509           0 :         ereport(ERROR,
     510             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     511             :                  errmsg("the log_filename parameter must equal 'postgresql-%%Y-%%m-%%d_%%H%%M%%S.log'")));
     512             : 
     513             :     /* check to see if caller supports us returning a tuplestore */
     514           0 :     if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
     515           0 :         ereport(ERROR,
     516             :                 (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
     517             :                  errmsg("set-valued function called in context that cannot accept a set")));
     518           0 :     if (!(rsinfo->allowedModes & SFRM_Materialize))
     519           0 :         ereport(ERROR,
     520             :                 (errcode(ERRCODE_SYNTAX_ERROR),
     521             :                  errmsg("materialize mode required, but it is not allowed in this context")));
     522             : 
     523             :     /* The tupdesc and tuplestore must be created in ecxt_per_query_memory */
     524           0 :     oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
     525             : 
     526           0 :     tupdesc = CreateTemplateTupleDesc(2);
     527           0 :     TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starttime",
     528             :                        TIMESTAMPOID, -1, 0);
     529           0 :     TupleDescInitEntry(tupdesc, (AttrNumber) 2, "filename",
     530             :                        TEXTOID, -1, 0);
     531             : 
     532           0 :     randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
     533           0 :     tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
     534           0 :     rsinfo->returnMode = SFRM_Materialize;
     535           0 :     rsinfo->setResult = tupstore;
     536           0 :     rsinfo->setDesc = tupdesc;
     537             : 
     538           0 :     MemoryContextSwitchTo(oldcontext);
     539             : 
     540           0 :     attinmeta = TupleDescGetAttInMetadata(tupdesc);
     541             : 
     542           0 :     dirdesc = AllocateDir(Log_directory);
     543           0 :     while ((de = ReadDir(dirdesc, Log_directory)) != NULL)
     544             :     {
     545             :         char       *values[2];
     546             :         HeapTuple   tuple;
     547             :         char        timestampbuf[32];
     548             :         char       *field[MAXDATEFIELDS];
     549             :         char        lowstr[MAXDATELEN + 1];
     550             :         int         dtype;
     551             :         int         nf,
     552             :                     ftype[MAXDATEFIELDS];
     553             :         fsec_t      fsec;
     554           0 :         int         tz = 0;
     555             :         struct pg_tm date;
     556             :         DateTimeErrorExtra extra;
     557             : 
     558             :         /*
     559             :          * Default format: postgresql-YYYY-MM-DD_HHMMSS.log
     560             :          */
     561           0 :         if (strlen(de->d_name) != 32
     562           0 :             || strncmp(de->d_name, "postgresql-", 11) != 0
     563           0 :             || de->d_name[21] != '_'
     564           0 :             || strcmp(de->d_name + 28, ".log") != 0)
     565           0 :             continue;
     566             : 
     567             :         /* extract timestamp portion of filename */
     568           0 :         strcpy(timestampbuf, de->d_name + 11);
     569           0 :         timestampbuf[17] = '\0';
     570             : 
     571             :         /* parse and decode expected timestamp to verify it's OK format */
     572           0 :         if (ParseDateTime(timestampbuf, lowstr, MAXDATELEN, field, ftype, MAXDATEFIELDS, &nf))
     573           0 :             continue;
     574             : 
     575           0 :         if (DecodeDateTime(field, ftype, nf,
     576             :                            &dtype, &date, &fsec, &tz, &extra))
     577           0 :             continue;
     578             : 
     579             :         /* Seems the timestamp is OK; prepare and return tuple */
     580             : 
     581           0 :         values[0] = timestampbuf;
     582           0 :         values[1] = psprintf("%s/%s", Log_directory, de->d_name);
     583             : 
     584           0 :         tuple = BuildTupleFromCStrings(attinmeta, values);
     585             : 
     586           0 :         tuplestore_puttuple(tupstore, tuple);
     587             :     }
     588             : 
     589           0 :     FreeDir(dirdesc);
     590           0 :     return (Datum) 0;
     591             : }

Generated by: LCOV version 1.14