adminpack.c 13.5 KB
Newer Older
Tom Lane's avatar
Tom Lane committed
1 2
/*-------------------------------------------------------------------------
 *
Neil Conway's avatar
Neil Conway committed
3
 * adminpack.c
Tom Lane's avatar
Tom Lane committed
4 5
 *
 *
Bruce Momjian's avatar
Bruce Momjian committed
6
 * Copyright (c) 2002-2018, PostgreSQL Global Development Group
Bruce Momjian's avatar
Bruce Momjian committed
7
 *
Tom Lane's avatar
Tom Lane committed
8 9 10
 * Author: Andreas Pflug <pgadmin@pse-consulting.de>
 *
 * IDENTIFICATION
11
 *	  contrib/adminpack/adminpack.c
Tom Lane's avatar
Tom Lane committed
12 13 14 15 16 17 18 19 20
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>

21
#include "catalog/pg_authid.h"
Tom Lane's avatar
Tom Lane committed
22 23
#include "catalog/pg_type.h"
#include "funcapi.h"
24
#include "miscadmin.h"
25
#include "postmaster/syslogger.h"
26
#include "storage/fd.h"
27
#include "utils/builtins.h"
Tom Lane's avatar
Tom Lane committed
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
#include "utils/datetime.h"


#ifdef WIN32

#ifdef rename
#undef rename
#endif

#ifdef unlink
#undef unlink
#endif
#endif

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(pg_file_write);
45
PG_FUNCTION_INFO_V1(pg_file_write_v1_1);
Tom Lane's avatar
Tom Lane committed
46
PG_FUNCTION_INFO_V1(pg_file_rename);
47
PG_FUNCTION_INFO_V1(pg_file_rename_v1_1);
Tom Lane's avatar
Tom Lane committed
48
PG_FUNCTION_INFO_V1(pg_file_unlink);
49
PG_FUNCTION_INFO_V1(pg_file_unlink_v1_1);
Tom Lane's avatar
Tom Lane committed
50
PG_FUNCTION_INFO_V1(pg_logdir_ls);
51 52 53 54 55
PG_FUNCTION_INFO_V1(pg_logdir_ls_v1_1);

static int64 pg_file_write_internal(text *file, text *data, bool replace);
static bool pg_file_rename_internal(text *file1, text *file2, text *file3);
static Datum pg_logdir_ls_internal(FunctionCallInfo fcinfo);
Tom Lane's avatar
Tom Lane committed
56

Bruce Momjian's avatar
Bruce Momjian committed
57
typedef struct
Tom Lane's avatar
Tom Lane committed
58
{
Bruce Momjian's avatar
Bruce Momjian committed
59 60
	char	   *location;
	DIR		   *dirdesc;
Tom Lane's avatar
Tom Lane committed
61 62 63 64 65 66 67
} directory_fctx;

/*-----------------------
 * some helper functions
 */

/*
68 69 70 71
 * Convert a "text" filename argument to C string, and check it's allowable.
 *
 * Filename may be absolute or relative to the DataDir, but we only allow
 * absolute paths that match DataDir or Log_directory.
Tom Lane's avatar
Tom Lane committed
72
 */
Bruce Momjian's avatar
Bruce Momjian committed
73
static char *
74
convert_and_check_filename(text *arg, bool logAllowed)
Tom Lane's avatar
Tom Lane committed
75
{
76
	char	   *filename = text_to_cstring(arg);
Tom Lane's avatar
Tom Lane committed
77

78
	canonicalize_path(filename);	/* filename can change length here */
Tom Lane's avatar
Tom Lane committed
79

80 81 82 83 84 85 86 87 88
	/*
	 * Members of the 'pg_write_server_files' role are allowed to access any
	 * files on the server as the PG user, so no need to do any further checks
	 * here.
	 */
	if (is_member_of_role(GetUserId(), DEFAULT_ROLE_WRITE_SERVER_FILES))
		return filename;

	/* User isn't a member of the default role, so check if it's allowable */
Tom Lane's avatar
Tom Lane committed
89 90
	if (is_absolute_path(filename))
	{
91 92 93
		/* Disallow '/a/b/data/..' */
		if (path_contains_parent_reference(filename))
			ereport(ERROR,
94
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
Tom Lane's avatar
Tom Lane committed
95
					 (errmsg("reference to parent directory (\"..\") not allowed"))));
96

97
		/*
98 99
		 * Allow absolute paths if within DataDir or Log_directory, even
		 * though Log_directory might be outside DataDir.
100 101 102 103 104
		 */
		if (!path_is_prefix_of_path(DataDir, filename) &&
			(!logAllowed || !is_absolute_path(Log_directory) ||
			 !path_is_prefix_of_path(Log_directory, filename)))
			ereport(ERROR,
105 106
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
					 (errmsg("absolute path not allowed"))));
Tom Lane's avatar
Tom Lane committed
107
	}
108 109 110 111 112 113
	else if (!path_is_relative_and_below_cwd(filename))
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 (errmsg("path must be in or below the current directory"))));

	return filename;
Tom Lane's avatar
Tom Lane committed
114 115 116 117 118 119 120 121 122 123
}


/*
 * check for superuser, bark if not.
 */
static void
requireSuperuser(void)
{
	if (!superuser())
Bruce Momjian's avatar
Bruce Momjian committed
124
		ereport(ERROR,
Tom Lane's avatar
Tom Lane committed
125
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
Tom Lane's avatar
Tom Lane committed
126
				 (errmsg("only superuser may access generic file functions"))));
Tom Lane's avatar
Tom Lane committed
127 128 129 130 131
}



/* ------------------------------------
132 133 134 135 136
 * pg_file_write - old version
 *
 * The superuser() check here must be kept as the library might be upgraded
 * without the extension being upgraded, meaning that in pre-1.1 installations
 * these functions could be called by any user.
Tom Lane's avatar
Tom Lane committed
137
 */
Bruce Momjian's avatar
Bruce Momjian committed
138 139
Datum
pg_file_write(PG_FUNCTION_ARGS)
Tom Lane's avatar
Tom Lane committed
140
{
141 142 143
	text	   *file = PG_GETARG_TEXT_PP(0);
	text	   *data = PG_GETARG_TEXT_PP(1);
	bool		replace = PG_GETARG_BOOL(2);
Bruce Momjian's avatar
Bruce Momjian committed
144
	int64		count = 0;
Tom Lane's avatar
Tom Lane committed
145 146 147

	requireSuperuser();

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
	count = pg_file_write_internal(file, data, replace);

	PG_RETURN_INT64(count);
}

/* ------------------------------------
 * pg_file_write_v1_1 - Version 1.1
 *
 * As of adminpack version 1.1, we no longer need to check if the user
 * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
 * Users can then grant access to it based on their policies.
 *
 * Otherwise identical to pg_file_write (above).
 */
Datum
pg_file_write_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file = PG_GETARG_TEXT_PP(0);
	text	   *data = PG_GETARG_TEXT_PP(1);
	bool		replace = PG_GETARG_BOOL(2);
	int64		count = 0;

	count = pg_file_write_internal(file, data, replace);

	PG_RETURN_INT64(count);
}
Tom Lane's avatar
Tom Lane committed
174

175 176 177 178 179
/* ------------------------------------
 * pg_file_write_internal - Workhorse for pg_file_write functions.
 *
 * This handles the actual work for pg_file_write.
 */
Tom Lane's avatar
Tom Lane committed
180
static int64
181 182 183 184 185 186 187 188 189
pg_file_write_internal(text *file, text *data, bool replace)
{
	FILE	   *f;
	char	   *filename;
	int64		count = 0;

	filename = convert_and_check_filename(file, false);

	if (!replace)
Tom Lane's avatar
Tom Lane committed
190
	{
Bruce Momjian's avatar
Bruce Momjian committed
191 192
		struct stat fst;

Tom Lane's avatar
Tom Lane committed
193
		if (stat(filename, &fst) >= 0)
Bruce Momjian's avatar
Bruce Momjian committed
194
			ereport(ERROR,
Tom Lane's avatar
Tom Lane committed
195
					(ERRCODE_DUPLICATE_FILE,
196
					 errmsg("file \"%s\" exists", filename)));
Tom Lane's avatar
Tom Lane committed
197

198
		f = AllocateFile(filename, "wb");
Tom Lane's avatar
Tom Lane committed
199 200
	}
	else
201
		f = AllocateFile(filename, "ab");
Tom Lane's avatar
Tom Lane committed
202 203 204 205

	if (!f)
		ereport(ERROR,
				(errcode_for_file_access(),
206 207
				 errmsg("could not open file \"%s\" for writing: %m",
						filename)));
Tom Lane's avatar
Tom Lane committed
208

209 210
	count = fwrite(VARDATA_ANY(data), 1, VARSIZE_ANY_EXHDR(data), f);
	if (count != VARSIZE_ANY_EXHDR(data) || FreeFile(f))
211 212 213
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write file \"%s\": %m", filename)));
Tom Lane's avatar
Tom Lane committed
214

215
	return (count);
Tom Lane's avatar
Tom Lane committed
216 217
}

218 219 220 221 222 223 224
/* ------------------------------------
 * pg_file_rename - old version
 *
 * The superuser() check here must be kept as the library might be upgraded
 * without the extension being upgraded, meaning that in pre-1.1 installations
 * these functions could be called by any user.
 */
Bruce Momjian's avatar
Bruce Momjian committed
225 226
Datum
pg_file_rename(PG_FUNCTION_ARGS)
Tom Lane's avatar
Tom Lane committed
227
{
228 229 230 231
	text	   *file1;
	text	   *file2;
	text	   *file3;
	bool		result;
Tom Lane's avatar
Tom Lane committed
232 233 234 235 236 237

	requireSuperuser();

	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
		PG_RETURN_NULL();

238 239 240
	file1 = PG_GETARG_TEXT_PP(0);
	file2 = PG_GETARG_TEXT_PP(1);

Tom Lane's avatar
Tom Lane committed
241
	if (PG_ARGISNULL(2))
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
		file3 = NULL;
	else
		file3 = PG_GETARG_TEXT_PP(2);

	result = pg_file_rename_internal(file1, file2, file3);

	PG_RETURN_BOOL(result);
}

/* ------------------------------------
 * pg_file_rename_v1_1 - Version 1.1
 *
 * As of adminpack version 1.1, we no longer need to check if the user
 * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
 * Users can then grant access to it based on their policies.
 *
 * Otherwise identical to pg_file_write (above).
 */
Datum
pg_file_rename_v1_1(PG_FUNCTION_ARGS)
{
	text	   *file1;
	text	   *file2;
	text	   *file3;
	bool		result;

	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
		PG_RETURN_NULL();

	file1 = PG_GETARG_TEXT_PP(0);
	file2 = PG_GETARG_TEXT_PP(1);

	if (PG_ARGISNULL(2))
		file3 = NULL;
	else
		file3 = PG_GETARG_TEXT_PP(2);

	result = pg_file_rename_internal(file1, file2, file3);

	PG_RETURN_BOOL(result);
}

/* ------------------------------------
 * pg_file_rename_internal - Workhorse for pg_file_rename functions.
 *
 * This handles the actual work for pg_file_rename.
 */
Tom Lane's avatar
Tom Lane committed
289
static bool
290 291 292 293 294 295 296 297 298 299 300
pg_file_rename_internal(text *file1, text *file2, text *file3)
{
	char	   *fn1,
			   *fn2,
			   *fn3;
	int			rc;

	fn1 = convert_and_check_filename(file1, false);
	fn2 = convert_and_check_filename(file2, false);

	if (file3 == NULL)
301
		fn3 = NULL;
Tom Lane's avatar
Tom Lane committed
302
	else
303
		fn3 = convert_and_check_filename(file3, false);
Tom Lane's avatar
Tom Lane committed
304 305 306 307 308

	if (access(fn1, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
309
				 errmsg("file \"%s\" is not accessible: %m", fn1)));
Tom Lane's avatar
Tom Lane committed
310

311
		return false;
Tom Lane's avatar
Tom Lane committed
312 313 314 315 316 317
	}

	if (fn3 && access(fn2, W_OK) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
318
				 errmsg("file \"%s\" is not accessible: %m", fn2)));
Tom Lane's avatar
Tom Lane committed
319

320
		return false;
Tom Lane's avatar
Tom Lane committed
321 322
	}

323
	rc = access(fn3 ? fn3 : fn2, W_OK);
Tom Lane's avatar
Tom Lane committed
324 325 326 327
	if (rc >= 0 || errno != ENOENT)
	{
		ereport(ERROR,
				(ERRCODE_DUPLICATE_FILE,
328 329
				 errmsg("cannot rename to target file \"%s\"",
						fn3 ? fn3 : fn2)));
Tom Lane's avatar
Tom Lane committed
330
	}
Bruce Momjian's avatar
Bruce Momjian committed
331

Tom Lane's avatar
Tom Lane committed
332 333
	if (fn3)
	{
Bruce Momjian's avatar
Bruce Momjian committed
334
		if (rename(fn2, fn3) != 0)
Tom Lane's avatar
Tom Lane committed
335 336 337
		{
			ereport(ERROR,
					(errcode_for_file_access(),
338 339
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn2, fn3)));
Tom Lane's avatar
Tom Lane committed
340 341 342 343 344
		}
		if (rename(fn1, fn2) != 0)
		{
			ereport(WARNING,
					(errcode_for_file_access(),
345 346
					 errmsg("could not rename \"%s\" to \"%s\": %m",
							fn1, fn2)));
Tom Lane's avatar
Tom Lane committed
347 348 349 350 351

			if (rename(fn3, fn2) != 0)
			{
				ereport(ERROR,
						(errcode_for_file_access(),
352 353
						 errmsg("could not rename \"%s\" back to \"%s\": %m",
								fn3, fn2)));
Tom Lane's avatar
Tom Lane committed
354 355 356 357 358
			}
			else
			{
				ereport(ERROR,
						(ERRCODE_UNDEFINED_FILE,
359 360
						 errmsg("renaming \"%s\" to \"%s\" was reverted",
								fn2, fn3)));
Tom Lane's avatar
Tom Lane committed
361 362 363 364 365 366 367
			}
		}
	}
	else if (rename(fn1, fn2) != 0)
	{
		ereport(ERROR,
				(errcode_for_file_access(),
368
				 errmsg("could not rename \"%s\" to \"%s\": %m", fn1, fn2)));
Tom Lane's avatar
Tom Lane committed
369 370
	}

371
	return true;
Tom Lane's avatar
Tom Lane committed
372 373 374
}


375 376 377 378 379 380 381
/* ------------------------------------
 * pg_file_unlink - old version
 *
 * The superuser() check here must be kept as the library might be upgraded
 * without the extension being upgraded, meaning that in pre-1.1 installations
 * these functions could be called by any user.
 */
Bruce Momjian's avatar
Bruce Momjian committed
382 383
Datum
pg_file_unlink(PG_FUNCTION_ARGS)
Tom Lane's avatar
Tom Lane committed
384
{
Bruce Momjian's avatar
Bruce Momjian committed
385
	char	   *filename;
Tom Lane's avatar
Tom Lane committed
386 387 388

	requireSuperuser();

389
	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0), false);
Tom Lane's avatar
Tom Lane committed
390 391 392

	if (access(filename, W_OK) < 0)
	{
Bruce Momjian's avatar
Bruce Momjian committed
393 394
		if (errno == ENOENT)
			PG_RETURN_BOOL(false);
Tom Lane's avatar
Tom Lane committed
395
		else
Bruce Momjian's avatar
Bruce Momjian committed
396
			ereport(ERROR,
Tom Lane's avatar
Tom Lane committed
397
					(errcode_for_file_access(),
398
					 errmsg("file \"%s\" is not accessible: %m", filename)));
Tom Lane's avatar
Tom Lane committed
399 400 401 402 403 404
	}

	if (unlink(filename) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
405
				 errmsg("could not unlink file \"%s\": %m", filename)));
Tom Lane's avatar
Tom Lane committed
406 407 408 409 410 411 412

		PG_RETURN_BOOL(false);
	}
	PG_RETURN_BOOL(true);
}


413 414 415 416 417 418 419 420 421
/* ------------------------------------
 * pg_file_unlink_v1_1 - Version 1.1
 *
 * As of adminpack version 1.1, we no longer need to check if the user
 * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
 * Users can then grant access to it based on their policies.
 *
 * Otherwise identical to pg_file_unlink (above).
 */
Bruce Momjian's avatar
Bruce Momjian committed
422
Datum
423
pg_file_unlink_v1_1(PG_FUNCTION_ARGS)
Tom Lane's avatar
Tom Lane committed
424
{
425 426 427 428 429 430 431 432 433 434 435 436 437
	char	   *filename;

	filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0), false);

	if (access(filename, W_OK) < 0)
	{
		if (errno == ENOENT)
			PG_RETURN_BOOL(false);
		else
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("file \"%s\" is not accessible: %m", filename)));
	}
Tom Lane's avatar
Tom Lane committed
438

439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
	if (unlink(filename) < 0)
	{
		ereport(WARNING,
				(errcode_for_file_access(),
				 errmsg("could not unlink file \"%s\": %m", filename)));

		PG_RETURN_BOOL(false);
	}
	PG_RETURN_BOOL(true);
}

/* ------------------------------------
 * pg_logdir_ls - Old version
 *
 * The superuser() check here must be kept as the library might be upgraded
 * without the extension being upgraded, meaning that in pre-1.1 installations
 * these functions could be called by any user.
 */
Datum
pg_logdir_ls(PG_FUNCTION_ARGS)
{
Bruce Momjian's avatar
Bruce Momjian committed
460
	if (!superuser())
Tom Lane's avatar
Tom Lane committed
461 462 463
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 (errmsg("only superuser can list the log directory"))));
Bruce Momjian's avatar
Bruce Momjian committed
464

465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
	return (pg_logdir_ls_internal(fcinfo));
}

/* ------------------------------------
 * pg_logdir_ls_v1_1 - Version 1.1
 *
 * As of adminpack version 1.1, we no longer need to check if the user
 * is a superuser because we REVOKE EXECUTE on the function from PUBLIC.
 * Users can then grant access to it based on their policies.
 *
 * Otherwise identical to pg_logdir_ls (above).
 */
Datum
pg_logdir_ls_v1_1(PG_FUNCTION_ARGS)
{
	return (pg_logdir_ls_internal(fcinfo));
}

Tom Lane's avatar
Tom Lane committed
483
static Datum
484 485 486 487 488 489
pg_logdir_ls_internal(FunctionCallInfo fcinfo)
{
	FuncCallContext *funcctx;
	struct dirent *de;
	directory_fctx *fctx;

490
	if (strcmp(Log_filename, "postgresql-%Y-%m-%d_%H%M%S.log") != 0)
Tom Lane's avatar
Tom Lane committed
491 492 493 494 495 496 497
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 (errmsg("the log_filename parameter must equal 'postgresql-%%Y-%%m-%%d_%%H%%M%%S.log'"))));

	if (SRF_IS_FIRSTCALL())
	{
		MemoryContext oldcontext;
Bruce Momjian's avatar
Bruce Momjian committed
498
		TupleDesc	tupdesc;
Tom Lane's avatar
Tom Lane committed
499

Bruce Momjian's avatar
Bruce Momjian committed
500
		funcctx = SRF_FIRSTCALL_INIT();
Tom Lane's avatar
Tom Lane committed
501 502 503
		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

		fctx = palloc(sizeof(directory_fctx));
504

505
		tupdesc = CreateTemplateTupleDesc(2);
Tom Lane's avatar
Tom Lane committed
506 507 508 509 510 511
		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starttime",
						   TIMESTAMPOID, -1, 0);
		TupleDescInitEntry(tupdesc, (AttrNumber) 2, "filename",
						   TEXTOID, -1, 0);

		funcctx->attinmeta = TupleDescGetAttInMetadata(tupdesc);
Bruce Momjian's avatar
Bruce Momjian committed
512

513
		fctx->location = pstrdup(Log_directory);
Tom Lane's avatar
Tom Lane committed
514 515 516
		fctx->dirdesc = AllocateDir(fctx->location);

		if (!fctx->dirdesc)
Bruce Momjian's avatar
Bruce Momjian committed
517
			ereport(ERROR,
Tom Lane's avatar
Tom Lane committed
518
					(errcode_for_file_access(),
519
					 errmsg("could not open directory \"%s\": %m",
520
							fctx->location)));
Tom Lane's avatar
Tom Lane committed
521 522 523 524 525

		funcctx->user_fctx = fctx;
		MemoryContextSwitchTo(oldcontext);
	}

Bruce Momjian's avatar
Bruce Momjian committed
526 527
	funcctx = SRF_PERCALL_SETUP();
	fctx = (directory_fctx *) funcctx->user_fctx;
Tom Lane's avatar
Tom Lane committed
528

529
	while ((de = ReadDir(fctx->dirdesc, fctx->location)) != NULL)
Tom Lane's avatar
Tom Lane committed
530
	{
Bruce Momjian's avatar
Bruce Momjian committed
531 532
		char	   *values[2];
		HeapTuple	tuple;
533
		char		timestampbuf[32];
Bruce Momjian's avatar
Bruce Momjian committed
534
		char	   *field[MAXDATEFIELDS];
Tom Lane's avatar
Tom Lane committed
535
		char		lowstr[MAXDATELEN + 1];
Bruce Momjian's avatar
Bruce Momjian committed
536 537 538
		int			dtype;
		int			nf,
					ftype[MAXDATEFIELDS];
Tom Lane's avatar
Tom Lane committed
539
		fsec_t		fsec;
Bruce Momjian's avatar
Bruce Momjian committed
540 541
		int			tz = 0;
		struct pg_tm date;
Tom Lane's avatar
Tom Lane committed
542 543

		/*
Bruce Momjian's avatar
Bruce Momjian committed
544
		 * Default format: postgresql-YYYY-MM-DD_HHMMSS.log
Tom Lane's avatar
Tom Lane committed
545 546
		 */
		if (strlen(de->d_name) != 32
547
			|| strncmp(de->d_name, "postgresql-", 11) != 0
Tom Lane's avatar
Tom Lane committed
548
			|| de->d_name[21] != '_'
549
			|| strcmp(de->d_name + 28, ".log") != 0)
Bruce Momjian's avatar
Bruce Momjian committed
550
			continue;
Tom Lane's avatar
Tom Lane committed
551

552 553 554
		/* extract timestamp portion of filename */
		strcpy(timestampbuf, de->d_name + 11);
		timestampbuf[17] = '\0';
Tom Lane's avatar
Tom Lane committed
555

556 557
		/* parse and decode expected timestamp to verify it's OK format */
		if (ParseDateTime(timestampbuf, lowstr, MAXDATELEN, field, ftype, MAXDATEFIELDS, &nf))
Bruce Momjian's avatar
Bruce Momjian committed
558
			continue;
Tom Lane's avatar
Tom Lane committed
559 560

		if (DecodeDateTime(field, ftype, nf, &dtype, &date, &fsec, &tz))
Bruce Momjian's avatar
Bruce Momjian committed
561
			continue;
Tom Lane's avatar
Tom Lane committed
562

563 564 565
		/* Seems the timestamp is OK; prepare and return tuple */

		values[0] = timestampbuf;
Peter Eisentraut's avatar
Peter Eisentraut committed
566
		values[1] = psprintf("%s/%s", fctx->location, de->d_name);
Tom Lane's avatar
Tom Lane committed
567 568 569 570 571 572 573 574 575

		tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);

		SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
	}

	FreeDir(fctx->dirdesc);
	SRF_RETURN_DONE(funcctx);
}