/*-------------------------------------------------------------------------
 *
 * parse_manifest.c
 *	  Parse a backup manifest in JSON format.
 *
 * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/bin/pg_verifybackup/parse_manifest.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres_fe.h"

#include "parse_manifest.h"
#include "common/jsonapi.h"

/*
 * Semantic states for JSON manifest parsing.
 */
typedef enum
{
	JM_EXPECT_TOPLEVEL_START,
	JM_EXPECT_TOPLEVEL_END,
	JM_EXPECT_TOPLEVEL_FIELD,
	JM_EXPECT_VERSION_VALUE,
	JM_EXPECT_FILES_START,
	JM_EXPECT_FILES_NEXT,
	JM_EXPECT_THIS_FILE_FIELD,
	JM_EXPECT_THIS_FILE_VALUE,
	JM_EXPECT_WAL_RANGES_START,
	JM_EXPECT_WAL_RANGES_NEXT,
	JM_EXPECT_THIS_WAL_RANGE_FIELD,
	JM_EXPECT_THIS_WAL_RANGE_VALUE,
	JM_EXPECT_MANIFEST_CHECKSUM_VALUE,
	JM_EXPECT_EOF
} JsonManifestSemanticState;

/*
 * Possible fields for one file as described by the manifest.
 */
typedef enum
{
	JMFF_PATH,
	JMFF_ENCODED_PATH,
	JMFF_SIZE,
	JMFF_LAST_MODIFIED,
	JMFF_CHECKSUM_ALGORITHM,
	JMFF_CHECKSUM
} JsonManifestFileField;

/*
 * Possible fields for one file as described by the manifest.
 */
typedef enum
{
	JMWRF_TIMELINE,
	JMWRF_START_LSN,
	JMWRF_END_LSN
} JsonManifestWALRangeField;

/*
 * Internal state used while decoding the JSON-format backup manifest.
 */
typedef struct
{
	JsonManifestParseContext *context;
	JsonManifestSemanticState state;

	/* These fields are used for parsing objects in the list of files. */
	JsonManifestFileField file_field;
	char	   *pathname;
	char	   *encoded_pathname;
	char	   *size;
	char	   *algorithm;
	pg_checksum_type checksum_algorithm;
	char	   *checksum;

	/* These fields are used for parsing objects in the list of WAL ranges. */
	JsonManifestWALRangeField wal_range_field;
	char	   *timeline;
	char	   *start_lsn;
	char	   *end_lsn;

	/* Miscellaneous other stuff. */
	bool		saw_version_field;
	char	   *manifest_checksum;
} JsonManifestParseState;

static JsonParseErrorType json_manifest_object_start(void *state);
static JsonParseErrorType json_manifest_object_end(void *state);
static JsonParseErrorType json_manifest_array_start(void *state);
static JsonParseErrorType json_manifest_array_end(void *state);
static JsonParseErrorType json_manifest_object_field_start(void *state, char *fname,
														   bool isnull);
static JsonParseErrorType json_manifest_scalar(void *state, char *token,
											   JsonTokenType tokentype);
static void json_manifest_finalize_file(JsonManifestParseState *parse);
static void json_manifest_finalize_wal_range(JsonManifestParseState *parse);
static void verify_manifest_checksum(JsonManifestParseState *parse,
									 char *buffer, size_t size);
static void json_manifest_parse_failure(JsonManifestParseContext *context,
										char *msg);

static int	hexdecode_char(char c);
static bool hexdecode_string(uint8 *result, char *input, int nbytes);
static bool parse_xlogrecptr(XLogRecPtr *result, char *input);

/*
 * Main entrypoint to parse a JSON-format backup manifest.
 *
 * Caller should set up the parsing context and then invoke this function.
 * For each file whose information is extracted from the manifest,
 * context->perfile_cb is invoked.  In case of trouble, context->error_cb is
 * invoked and is expected not to return.
 */
void
json_parse_manifest(JsonManifestParseContext *context, char *buffer,
					size_t size)
{
	JsonLexContext *lex;
	JsonParseErrorType json_error;
	JsonSemAction sem;
	JsonManifestParseState parse;

	/* Set up our private parsing context. */
	parse.context = context;
	parse.state = JM_EXPECT_TOPLEVEL_START;
	parse.saw_version_field = false;

	/* Create a JSON lexing context. */
	lex = makeJsonLexContextCstringLen(buffer, size, PG_UTF8, true);

	/* Set up semantic actions. */
	sem.semstate = &parse;
	sem.object_start = json_manifest_object_start;
	sem.object_end = json_manifest_object_end;
	sem.array_start = json_manifest_array_start;
	sem.array_end = json_manifest_array_end;
	sem.object_field_start = json_manifest_object_field_start;
	sem.object_field_end = NULL;
	sem.array_element_start = NULL;
	sem.array_element_end = NULL;
	sem.scalar = json_manifest_scalar;

	/* Run the actual JSON parser. */
	json_error = pg_parse_json(lex, &sem);
	if (json_error != JSON_SUCCESS)
		json_manifest_parse_failure(context, "parsing failed");
	if (parse.state != JM_EXPECT_EOF)
		json_manifest_parse_failure(context, "manifest ended unexpectedly");

	/* Verify the manifest checksum. */
	verify_manifest_checksum(&parse, buffer, size);
}

/*
 * Invoked at the start of each object in the JSON document.
 *
 * The document as a whole is expected to be an object; each file and each
 * WAL range is also expected to be an object. If we're anywhere else in the
 * document, it's an error.
 */
static JsonParseErrorType
json_manifest_object_start(void *state)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_TOPLEVEL_START:
			parse->state = JM_EXPECT_TOPLEVEL_FIELD;
			break;
		case JM_EXPECT_FILES_NEXT:
			parse->state = JM_EXPECT_THIS_FILE_FIELD;
			parse->pathname = NULL;
			parse->encoded_pathname = NULL;
			parse->size = NULL;
			parse->algorithm = NULL;
			parse->checksum = NULL;
			break;
		case JM_EXPECT_WAL_RANGES_NEXT:
			parse->state = JM_EXPECT_THIS_WAL_RANGE_FIELD;
			parse->timeline = NULL;
			parse->start_lsn = NULL;
			parse->end_lsn = NULL;
			break;
		default:
			json_manifest_parse_failure(parse->context,
										"unexpected object start");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Invoked at the end of each object in the JSON document.
 *
 * The possible cases here are the same as for json_manifest_object_start.
 * There's nothing special to do at the end of the document, but when we
 * reach the end of an object representing a particular file or WAL range,
 * we must call json_manifest_finalize_file() to save the associated details.
 */
static JsonParseErrorType
json_manifest_object_end(void *state)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_TOPLEVEL_END:
			parse->state = JM_EXPECT_EOF;
			break;
		case JM_EXPECT_THIS_FILE_FIELD:
			json_manifest_finalize_file(parse);
			parse->state = JM_EXPECT_FILES_NEXT;
			break;
		case JM_EXPECT_THIS_WAL_RANGE_FIELD:
			json_manifest_finalize_wal_range(parse);
			parse->state = JM_EXPECT_WAL_RANGES_NEXT;
			break;
		default:
			json_manifest_parse_failure(parse->context,
										"unexpected object end");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Invoked at the start of each array in the JSON document.
 *
 * Within the toplevel object, the value associated with the "Files" key
 * should be an array. Similarly for the "WAL-Ranges" key. No other arrays
 * are expected.
 */
static JsonParseErrorType
json_manifest_array_start(void *state)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_FILES_START:
			parse->state = JM_EXPECT_FILES_NEXT;
			break;
		case JM_EXPECT_WAL_RANGES_START:
			parse->state = JM_EXPECT_WAL_RANGES_NEXT;
			break;
		default:
			json_manifest_parse_failure(parse->context,
										"unexpected array start");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Invoked at the end of each array in the JSON document.
 *
 * The cases here are analogous to those in json_manifest_array_start.
 */
static JsonParseErrorType
json_manifest_array_end(void *state)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_FILES_NEXT:
		case JM_EXPECT_WAL_RANGES_NEXT:
			parse->state = JM_EXPECT_TOPLEVEL_FIELD;
			break;
		default:
			json_manifest_parse_failure(parse->context,
										"unexpected array end");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Invoked at the start of each object field in the JSON document.
 */
static JsonParseErrorType
json_manifest_object_field_start(void *state, char *fname, bool isnull)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_TOPLEVEL_FIELD:

			/*
			 * Inside toplevel object. The version indicator should always be
			 * the first field.
			 */
			if (!parse->saw_version_field)
			{
				if (strcmp(fname, "PostgreSQL-Backup-Manifest-Version") != 0)
					json_manifest_parse_failure(parse->context,
												"expected version indicator");
				parse->state = JM_EXPECT_VERSION_VALUE;
				parse->saw_version_field = true;
				break;
			}

			/* Is this the list of files? */
			if (strcmp(fname, "Files") == 0)
			{
				parse->state = JM_EXPECT_FILES_START;
				break;
			}

			/* Is this the list of WAL ranges? */
			if (strcmp(fname, "WAL-Ranges") == 0)
			{
				parse->state = JM_EXPECT_WAL_RANGES_START;
				break;
			}

			/* Is this the manifest checksum? */
			if (strcmp(fname, "Manifest-Checksum") == 0)
			{
				parse->state = JM_EXPECT_MANIFEST_CHECKSUM_VALUE;
				break;
			}

			/* It's not a field we recognize. */
			json_manifest_parse_failure(parse->context,
										"unrecognized top-level field");
			break;

		case JM_EXPECT_THIS_FILE_FIELD:
			/* Inside object for one file; which key have we got? */
			if (strcmp(fname, "Path") == 0)
				parse->file_field = JMFF_PATH;
			else if (strcmp(fname, "Encoded-Path") == 0)
				parse->file_field = JMFF_ENCODED_PATH;
			else if (strcmp(fname, "Size") == 0)
				parse->file_field = JMFF_SIZE;
			else if (strcmp(fname, "Last-Modified") == 0)
				parse->file_field = JMFF_LAST_MODIFIED;
			else if (strcmp(fname, "Checksum-Algorithm") == 0)
				parse->file_field = JMFF_CHECKSUM_ALGORITHM;
			else if (strcmp(fname, "Checksum") == 0)
				parse->file_field = JMFF_CHECKSUM;
			else
				json_manifest_parse_failure(parse->context,
											"unexpected file field");
			parse->state = JM_EXPECT_THIS_FILE_VALUE;
			break;

		case JM_EXPECT_THIS_WAL_RANGE_FIELD:
			/* Inside object for one file; which key have we got? */
			if (strcmp(fname, "Timeline") == 0)
				parse->wal_range_field = JMWRF_TIMELINE;
			else if (strcmp(fname, "Start-LSN") == 0)
				parse->wal_range_field = JMWRF_START_LSN;
			else if (strcmp(fname, "End-LSN") == 0)
				parse->wal_range_field = JMWRF_END_LSN;
			else
				json_manifest_parse_failure(parse->context,
											"unexpected WAL range field");
			parse->state = JM_EXPECT_THIS_WAL_RANGE_VALUE;
			break;

		default:
			json_manifest_parse_failure(parse->context,
										"unexpected object field");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Invoked at the start of each scalar in the JSON document.
 *
 * Object field names don't reach this code; those are handled by
 * json_manifest_object_field_start. When we're inside of the object for
 * a particular file or WAL range, that function will have noticed the name
 * of the field, and we'll get the corresponding value here. When we're in
 * the toplevel object, the parse state itself tells us which field this is.
 *
 * In all cases except for PostgreSQL-Backup-Manifest-Version, which we
 * can just check on the spot, the goal here is just to save the value in
 * the parse state for later use. We don't actually do anything until we
 * reach either the end of the object representing this file, or the end
 * of the manifest, as the case may be.
 */
static JsonParseErrorType
json_manifest_scalar(void *state, char *token, JsonTokenType tokentype)
{
	JsonManifestParseState *parse = state;

	switch (parse->state)
	{
		case JM_EXPECT_VERSION_VALUE:
			if (strcmp(token, "1") != 0)
				json_manifest_parse_failure(parse->context,
											"unexpected manifest version");
			parse->state = JM_EXPECT_TOPLEVEL_FIELD;
			break;

		case JM_EXPECT_THIS_FILE_VALUE:
			switch (parse->file_field)
			{
				case JMFF_PATH:
					parse->pathname = token;
					break;
				case JMFF_ENCODED_PATH:
					parse->encoded_pathname = token;
					break;
				case JMFF_SIZE:
					parse->size = token;
					break;
				case JMFF_LAST_MODIFIED:
					pfree(token);	/* unused */
					break;
				case JMFF_CHECKSUM_ALGORITHM:
					parse->algorithm = token;
					break;
				case JMFF_CHECKSUM:
					parse->checksum = token;
					break;
			}
			parse->state = JM_EXPECT_THIS_FILE_FIELD;
			break;

		case JM_EXPECT_THIS_WAL_RANGE_VALUE:
			switch (parse->wal_range_field)
			{
				case JMWRF_TIMELINE:
					parse->timeline = token;
					break;
				case JMWRF_START_LSN:
					parse->start_lsn = token;
					break;
				case JMWRF_END_LSN:
					parse->end_lsn = token;
					break;
			}
			parse->state = JM_EXPECT_THIS_WAL_RANGE_FIELD;
			break;

		case JM_EXPECT_MANIFEST_CHECKSUM_VALUE:
			parse->state = JM_EXPECT_TOPLEVEL_END;
			parse->manifest_checksum = token;
			break;

		default:
			json_manifest_parse_failure(parse->context, "unexpected scalar");
			break;
	}

	return JSON_SUCCESS;
}

/*
 * Do additional parsing and sanity-checking of the details gathered for one
 * file, and invoke the per-file callback so that the caller gets those
 * details. This happens for each file when the corresponding JSON object is
 * completely parsed.
 */
static void
json_manifest_finalize_file(JsonManifestParseState *parse)
{
	JsonManifestParseContext *context = parse->context;
	size_t		size;
	char	   *ep;
	int			checksum_string_length;
	pg_checksum_type checksum_type;
	int			checksum_length;
	uint8	   *checksum_payload;

	/* Pathname and size are required. */
	if (parse->pathname == NULL && parse->encoded_pathname == NULL)
		json_manifest_parse_failure(parse->context, "missing path name");
	if (parse->pathname != NULL && parse->encoded_pathname != NULL)
		json_manifest_parse_failure(parse->context,
									"both path name and encoded path name");
	if (parse->size == NULL)
		json_manifest_parse_failure(parse->context, "missing size");
	if (parse->algorithm == NULL && parse->checksum != NULL)
		json_manifest_parse_failure(parse->context,
									"checksum without algorithm");

	/* Decode encoded pathname, if that's what we have. */
	if (parse->encoded_pathname != NULL)
	{
		int			encoded_length = strlen(parse->encoded_pathname);
		int			raw_length = encoded_length / 2;

		parse->pathname = palloc(raw_length + 1);
		if (encoded_length % 2 != 0 ||
			!hexdecode_string((uint8 *) parse->pathname,
							  parse->encoded_pathname,
							  raw_length))
			json_manifest_parse_failure(parse->context,
										"could not decode file name");
		parse->pathname[raw_length] = '\0';
		pfree(parse->encoded_pathname);
		parse->encoded_pathname = NULL;
	}

	/* Parse size. */
	size = strtoul(parse->size, &ep, 10);
	if (*ep)
		json_manifest_parse_failure(parse->context,
									"file size is not an integer");

	/* Parse the checksum algorithm, if it's present. */
	if (parse->algorithm == NULL)
		checksum_type = CHECKSUM_TYPE_NONE;
	else if (!pg_checksum_parse_type(parse->algorithm, &checksum_type))
		context->error_cb(context, "unrecognized checksum algorithm: \"%s\"",
						  parse->algorithm);

	/* Parse the checksum payload, if it's present. */
	checksum_string_length = parse->checksum == NULL ? 0
		: strlen(parse->checksum);
	if (checksum_string_length == 0)
	{
		checksum_length = 0;
		checksum_payload = NULL;
	}
	else
	{
		checksum_length = checksum_string_length / 2;
		checksum_payload = palloc(checksum_length);
		if (checksum_string_length % 2 != 0 ||
			!hexdecode_string(checksum_payload, parse->checksum,
							  checksum_length))
			context->error_cb(context,
							  "invalid checksum for file \"%s\": \"%s\"",
							  parse->pathname, parse->checksum);
	}

	/* Invoke the callback with the details we've gathered. */
	context->perfile_cb(context, parse->pathname, size,
						checksum_type, checksum_length, checksum_payload);

	/* Free memory we no longer need. */
	if (parse->size != NULL)
	{
		pfree(parse->size);
		parse->size = NULL;
	}
	if (parse->algorithm != NULL)
	{
		pfree(parse->algorithm);
		parse->algorithm = NULL;
	}
	if (parse->checksum != NULL)
	{
		pfree(parse->checksum);
		parse->checksum = NULL;
	}
}

/*
 * Do additional parsing and sanity-checking of the details gathered for one
 * WAL range, and invoke the per-WAL-range callback so that the caller gets
 * those details. This happens for each WAL range when the corresponding JSON
 * object is completely parsed.
 */
static void
json_manifest_finalize_wal_range(JsonManifestParseState *parse)
{
	JsonManifestParseContext *context = parse->context;
	TimeLineID	tli;
	XLogRecPtr	start_lsn,
				end_lsn;
	char	   *ep;

	/* Make sure all fields are present. */
	if (parse->timeline == NULL)
		json_manifest_parse_failure(parse->context, "missing timeline");
	if (parse->start_lsn == NULL)
		json_manifest_parse_failure(parse->context, "missing start LSN");
	if (parse->end_lsn == NULL)
		json_manifest_parse_failure(parse->context, "missing end LSN");

	/* Parse timeline. */
	tli = strtoul(parse->timeline, &ep, 10);
	if (*ep)
		json_manifest_parse_failure(parse->context,
									"timeline is not an integer");
	if (!parse_xlogrecptr(&start_lsn, parse->start_lsn))
		json_manifest_parse_failure(parse->context,
									"could not parse start LSN");
	if (!parse_xlogrecptr(&end_lsn, parse->end_lsn))
		json_manifest_parse_failure(parse->context,
									"could not parse end LSN");

	/* Invoke the callback with the details we've gathered. */
	context->perwalrange_cb(context, tli, start_lsn, end_lsn);

	/* Free memory we no longer need. */
	if (parse->timeline != NULL)
	{
		pfree(parse->timeline);
		parse->timeline = NULL;
	}
	if (parse->start_lsn != NULL)
	{
		pfree(parse->start_lsn);
		parse->start_lsn = NULL;
	}
	if (parse->end_lsn != NULL)
	{
		pfree(parse->end_lsn);
		parse->end_lsn = NULL;
	}
}

/*
 * Verify that the manifest checksum is correct.
 *
 * The last line of the manifest file is excluded from the manifest checksum,
 * because the last line is expected to contain the checksum that covers
 * the rest of the file.
 */
static void
verify_manifest_checksum(JsonManifestParseState *parse, char *buffer,
						 size_t size)
{
	JsonManifestParseContext *context = parse->context;
	size_t		i;
	size_t		number_of_newlines = 0;
	size_t		ultimate_newline = 0;
	size_t		penultimate_newline = 0;
	pg_cryptohash_ctx *manifest_ctx;
	uint8		manifest_checksum_actual[PG_SHA256_DIGEST_LENGTH];
	uint8		manifest_checksum_expected[PG_SHA256_DIGEST_LENGTH];

	/* Find the last two newlines in the file. */
	for (i = 0; i < size; ++i)
	{
		if (buffer[i] == '\n')
		{
			++number_of_newlines;
			penultimate_newline = ultimate_newline;
			ultimate_newline = i;
		}
	}

	/*
	 * Make sure that the last newline is right at the end, and that there are
	 * at least two lines total. We need this to be true in order for the
	 * following code, which computes the manifest checksum, to work properly.
	 */
	if (number_of_newlines < 2)
		json_manifest_parse_failure(parse->context,
									"expected at least 2 lines");
	if (ultimate_newline != size - 1)
		json_manifest_parse_failure(parse->context,
									"last line not newline-terminated");

	/* Checksum the rest. */
	manifest_ctx = pg_cryptohash_create(PG_SHA256);
	if (manifest_ctx == NULL)
		context->error_cb(context, "out of memory");
	if (pg_cryptohash_init(manifest_ctx) < 0)
		context->error_cb(context, "could not initialize checksum of manifest");
	if (pg_cryptohash_update(manifest_ctx, (uint8 *) buffer, penultimate_newline + 1) < 0)
		context->error_cb(context, "could not update checksum of manifest");
	if (pg_cryptohash_final(manifest_ctx, manifest_checksum_actual,
							sizeof(manifest_checksum_actual)) < 0)
		context->error_cb(context, "could not finalize checksum of manifest");

	/* Now verify it. */
	if (parse->manifest_checksum == NULL)
		context->error_cb(parse->context, "manifest has no checksum");
	if (strlen(parse->manifest_checksum) != PG_SHA256_DIGEST_LENGTH * 2 ||
		!hexdecode_string(manifest_checksum_expected, parse->manifest_checksum,
						  PG_SHA256_DIGEST_LENGTH))
		context->error_cb(context, "invalid manifest checksum: \"%s\"",
						  parse->manifest_checksum);
	if (memcmp(manifest_checksum_actual, manifest_checksum_expected,
			   PG_SHA256_DIGEST_LENGTH) != 0)
		context->error_cb(context, "manifest checksum mismatch");
	pg_cryptohash_free(manifest_ctx);
}

/*
 * Report a parse error.
 *
 * This is intended to be used for fairly low-level failures that probably
 * shouldn't occur unless somebody has deliberately constructed a bad manifest,
 * or unless the server is generating bad manifests due to some bug. msg should
 * be a short string giving some hint as to what the problem is.
 */
static void
json_manifest_parse_failure(JsonManifestParseContext *context, char *msg)
{
	context->error_cb(context, "could not parse backup manifest: %s", msg);
}

/*
 * Convert a character which represents a hexadecimal digit to an integer.
 *
 * Returns -1 if the character is not a hexadecimal digit.
 */
static int
hexdecode_char(char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;

	return -1;
}

/*
 * Decode a hex string into a byte string, 2 hex chars per byte.
 *
 * Returns false if invalid characters are encountered; otherwise true.
 */
static bool
hexdecode_string(uint8 *result, char *input, int nbytes)
{
	int			i;

	for (i = 0; i < nbytes; ++i)
	{
		int			n1 = hexdecode_char(input[i * 2]);
		int			n2 = hexdecode_char(input[i * 2 + 1]);

		if (n1 < 0 || n2 < 0)
			return false;
		result[i] = n1 * 16 + n2;
	}

	return true;
}

/*
 * Parse an XLogRecPtr expressed using the usual string format.
 */
static bool
parse_xlogrecptr(XLogRecPtr *result, char *input)
{
	uint32		hi;
	uint32		lo;

	if (sscanf(input, "%X/%X", &hi, &lo) != 2)
		return false;
	*result = ((uint64) hi) << 32 | lo;
	return true;
}
