/*-------------------------------------------------------------------------
 *
 * Write a new backup manifest.
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/bin/pg_combinebackup/write_manifest.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres_fe.h"

#include <fcntl.h>
#include <time.h>
#include <unistd.h>

#include "common/checksum_helper.h"
#include "common/file_perm.h"
#include "common/logging.h"
#include "lib/stringinfo.h"
#include "load_manifest.h"
#include "mb/pg_wchar.h"
#include "write_manifest.h"

struct manifest_writer
{
	char		pathname[MAXPGPATH];
	int			fd;
	StringInfoData buf;
	bool		first_file;
	bool		still_checksumming;
	pg_checksum_context manifest_ctx;
};

static void escape_json(StringInfo buf, const char *str);
static void flush_manifest(manifest_writer *mwriter);
static size_t hex_encode(const uint8 *src, size_t len, char *dst);

/*
 * Create a new backup manifest writer.
 *
 * The backup manifest will be written into a file named backup_manifest
 * in the specified directory.
 */
manifest_writer *
create_manifest_writer(char *directory, uint64 system_identifier)
{
	manifest_writer *mwriter = pg_malloc(sizeof(manifest_writer));

	snprintf(mwriter->pathname, MAXPGPATH, "%s/backup_manifest", directory);
	mwriter->fd = -1;
	initStringInfo(&mwriter->buf);
	mwriter->first_file = true;
	mwriter->still_checksumming = true;
	pg_checksum_init(&mwriter->manifest_ctx, CHECKSUM_TYPE_SHA256);

	appendStringInfo(&mwriter->buf,
					 "{ \"PostgreSQL-Backup-Manifest-Version\": 2,\n"
					 "\"System-Identifier\": " UINT64_FORMAT ",\n"
					 "\"Files\": [",
					 system_identifier);

	return mwriter;
}

/*
 * Add an entry for a file to a backup manifest.
 *
 * This is very similar to the backend's AddFileToBackupManifest, but
 * various adjustments are required due to frontend/backend differences
 * and other details.
 */
void
add_file_to_manifest(manifest_writer *mwriter, const char *manifest_path,
					 uint64 size, time_t mtime,
					 pg_checksum_type checksum_type,
					 int checksum_length,
					 uint8 *checksum_payload)
{
	int			pathlen = strlen(manifest_path);

	if (mwriter->first_file)
	{
		appendStringInfoChar(&mwriter->buf, '\n');
		mwriter->first_file = false;
	}
	else
		appendStringInfoString(&mwriter->buf, ",\n");

	if (pg_encoding_verifymbstr(PG_UTF8, manifest_path, pathlen) == pathlen)
	{
		appendStringInfoString(&mwriter->buf, "{ \"Path\": ");
		escape_json(&mwriter->buf, manifest_path);
		appendStringInfoString(&mwriter->buf, ", ");
	}
	else
	{
		appendStringInfoString(&mwriter->buf, "{ \"Encoded-Path\": \"");
		enlargeStringInfo(&mwriter->buf, 2 * pathlen);
		mwriter->buf.len += hex_encode((const uint8 *) manifest_path, pathlen,
									   &mwriter->buf.data[mwriter->buf.len]);
		appendStringInfoString(&mwriter->buf, "\", ");
	}

	appendStringInfo(&mwriter->buf, "\"Size\": %" PRIu64 ", ", size);

	appendStringInfoString(&mwriter->buf, "\"Last-Modified\": \"");
	enlargeStringInfo(&mwriter->buf, 128);
	mwriter->buf.len += strftime(&mwriter->buf.data[mwriter->buf.len], 128,
								 "%Y-%m-%d %H:%M:%S %Z",
								 gmtime(&mtime));
	appendStringInfoChar(&mwriter->buf, '"');

	if (mwriter->buf.len > 128 * 1024)
		flush_manifest(mwriter);

	if (checksum_length > 0)
	{
		appendStringInfo(&mwriter->buf,
						 ", \"Checksum-Algorithm\": \"%s\", \"Checksum\": \"",
						 pg_checksum_type_name(checksum_type));

		enlargeStringInfo(&mwriter->buf, 2 * checksum_length);
		mwriter->buf.len += hex_encode(checksum_payload, checksum_length,
									   &mwriter->buf.data[mwriter->buf.len]);

		appendStringInfoChar(&mwriter->buf, '"');
	}

	appendStringInfoString(&mwriter->buf, " }");

	if (mwriter->buf.len > 128 * 1024)
		flush_manifest(mwriter);
}

/*
 * Finalize the backup_manifest.
 */
void
finalize_manifest(manifest_writer *mwriter,
				  manifest_wal_range *first_wal_range)
{
	uint8		checksumbuf[PG_SHA256_DIGEST_LENGTH];
	int			len;
	manifest_wal_range *wal_range;

	/* Terminate the list of files. */
	appendStringInfoString(&mwriter->buf, "\n],\n");

	/* Start a list of LSN ranges. */
	appendStringInfoString(&mwriter->buf, "\"WAL-Ranges\": [\n");

	for (wal_range = first_wal_range; wal_range != NULL;
		 wal_range = wal_range->next)
		appendStringInfo(&mwriter->buf,
						 "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%X\", \"End-LSN\": \"%X/%X\" }",
						 wal_range == first_wal_range ? "" : ",\n",
						 wal_range->tli,
						 LSN_FORMAT_ARGS(wal_range->start_lsn),
						 LSN_FORMAT_ARGS(wal_range->end_lsn));

	/* Terminate the list of WAL ranges. */
	appendStringInfoString(&mwriter->buf, "\n],\n");

	/* Flush accumulated data and update checksum calculation. */
	flush_manifest(mwriter);

	/* Checksum only includes data up to this point. */
	mwriter->still_checksumming = false;

	/* Compute and insert manifest checksum. */
	appendStringInfoString(&mwriter->buf, "\"Manifest-Checksum\": \"");
	enlargeStringInfo(&mwriter->buf, 2 * PG_SHA256_DIGEST_STRING_LENGTH);
	len = pg_checksum_final(&mwriter->manifest_ctx, checksumbuf);
	Assert(len == PG_SHA256_DIGEST_LENGTH);
	mwriter->buf.len +=
		hex_encode(checksumbuf, len, &mwriter->buf.data[mwriter->buf.len]);
	appendStringInfoString(&mwriter->buf, "\"}\n");

	/* Flush the last manifest checksum itself. */
	flush_manifest(mwriter);

	/* Close the file. */
	if (close(mwriter->fd) != 0)
		pg_fatal("could not close file \"%s\": %m", mwriter->pathname);
	mwriter->fd = -1;
}

/*
 * Produce a JSON string literal, properly escaping characters in the text.
 */
static void
escape_json(StringInfo buf, const char *str)
{
	const char *p;

	appendStringInfoCharMacro(buf, '"');
	for (p = str; *p; p++)
	{
		switch (*p)
		{
			case '\b':
				appendStringInfoString(buf, "\\b");
				break;
			case '\f':
				appendStringInfoString(buf, "\\f");
				break;
			case '\n':
				appendStringInfoString(buf, "\\n");
				break;
			case '\r':
				appendStringInfoString(buf, "\\r");
				break;
			case '\t':
				appendStringInfoString(buf, "\\t");
				break;
			case '"':
				appendStringInfoString(buf, "\\\"");
				break;
			case '\\':
				appendStringInfoString(buf, "\\\\");
				break;
			default:
				if ((unsigned char) *p < ' ')
					appendStringInfo(buf, "\\u%04x", (int) *p);
				else
					appendStringInfoCharMacro(buf, *p);
				break;
		}
	}
	appendStringInfoCharMacro(buf, '"');
}

/*
 * Flush whatever portion of the backup manifest we have generated and
 * buffered in memory out to a file on disk.
 *
 * The first call to this function will create the file. After that, we
 * keep it open and just append more data.
 */
static void
flush_manifest(manifest_writer *mwriter)
{
	if (mwriter->fd == -1 &&
		(mwriter->fd = open(mwriter->pathname,
							O_WRONLY | O_CREAT | O_EXCL | PG_BINARY,
							pg_file_create_mode)) < 0)
		pg_fatal("could not open file \"%s\": %m", mwriter->pathname);

	if (mwriter->buf.len > 0)
	{
		ssize_t		wb;

		wb = write(mwriter->fd, mwriter->buf.data, mwriter->buf.len);
		if (wb != mwriter->buf.len)
		{
			if (wb < 0)
				pg_fatal("could not write file \"%s\": %m", mwriter->pathname);
			else
				pg_fatal("could not write file \"%s\": wrote %d of %d",
						 mwriter->pathname, (int) wb, mwriter->buf.len);
		}

		if (mwriter->still_checksumming &&
			pg_checksum_update(&mwriter->manifest_ctx,
							   (uint8 *) mwriter->buf.data,
							   mwriter->buf.len) < 0)
			pg_fatal("could not update checksum of file \"%s\"",
					 mwriter->pathname);
		resetStringInfo(&mwriter->buf);
	}
}

/*
 * Encode bytes using two hexadecimal digits for each one.
 */
static size_t
hex_encode(const uint8 *src, size_t len, char *dst)
{
	const uint8 *end = src + len;

	while (src < end)
	{
		unsigned	n1 = (*src >> 4) & 0xF;
		unsigned	n2 = *src & 0xF;

		*dst++ = n1 < 10 ? '0' + n1 : 'a' + n1 - 10;
		*dst++ = n2 < 10 ? '0' + n2 : 'a' + n2 - 10;
		++src;
	}

	return len * 2;
}
