/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "hex-dec.h"
#include "istream.h"
#include "index/dbox-common/dbox-file.h"
#include "doveadm-dump.h"

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

static void
dump_timestamp(struct istream *input, const char *name, const char *value)
{
	time_t t;

	if (strcmp(value, "0") == 0)
		t = 0;
	else {
		t = hex2dec((const void *)value, strlen(value));
		if (t == 0) {
			i_fatal("Invalid %s at %"PRIuUOFF_T": %s",
				name, input->v_offset, value);
		}
	}
	printf("%s = %ld (%s)\n", name, (long)t, unixdate2str(t));
}

static uoff_t
dump_size(struct istream *input, const char *name, const char *value)
{
	uoff_t size;

	if (strcmp(value, "0") == 0)
		size = 0;
	else {
		size = hex2dec((const void *)value, strlen(value));
		if (size == 0) {
			i_fatal("Invalid %s at %"PRIuUOFF_T": %s",
				name, input->v_offset, value);
		}
	}
	printf("%s = %"PRIuUOFF_T"\n", name, size);
	return size;
}

static unsigned int dump_file_hdr(struct istream *input)
{
	const char *line, *const *arg, *version;
	unsigned int msg_hdr_size = 0;

	if ((line = i_stream_read_next_line(input)) == NULL)
		i_fatal("Empty file");
	arg = t_strsplit(line, " ");

	/* check version */
	version = *arg;
	if (version == NULL || !str_is_numeric(version, ' '))
		i_fatal("%s is not a dbox file", i_stream_get_name(input));
	if (strcmp(version, "2") != 0)
		i_fatal("Unsupported dbox file version %s", version);
	arg++;

	for (; *arg != NULL; arg++) {
		switch (**arg) {
		case DBOX_HEADER_MSG_HEADER_SIZE:
			msg_hdr_size = hex2dec((const void *)(*arg + 1),
					       strlen(*arg + 1));
			if (msg_hdr_size == 0) {
				i_fatal("Invalid msg_header_size header: %s",
					*arg + 1);
			}
			printf("file.msg_header_size = %u\n", msg_hdr_size);
			break;
		case DBOX_HEADER_CREATE_STAMP:
			dump_timestamp(input, "file.create_stamp", *arg + 1);
			break;
		default:
			printf("file.unknown-%c = %s\n", **arg, *arg + 1);
			break;
		}
	}
	if (msg_hdr_size == 0)
		i_fatal("Missing msg_header_size in file header");
	return msg_hdr_size;
}

static bool
dump_msg_hdr(struct istream *input, unsigned int hdr_size, uoff_t *msg_size_r)
{
	struct dbox_message_header hdr;
	const unsigned char *data;
	size_t size;
	uoff_t msg_size;

	if (i_stream_read_bytes(input, &data, &size, hdr_size) <= 0) {
		if (size == 0)
			return FALSE;
		i_fatal("Partial message header read at %"PRIuUOFF_T": %zu bytes",
			input->v_offset, size);
	}
	printf("offset %"PRIuUOFF_T":\n", input->v_offset);

	if (hdr_size < sizeof(hdr))
		i_fatal("file.hdr_size too small: %u", hdr_size);
	memcpy(&hdr, data, sizeof(hdr));

	if (memcmp(hdr.magic_pre, DBOX_MAGIC_PRE, sizeof(hdr.magic_pre)) != 0)
		i_fatal("dbox wrong pre-magic at %"PRIuUOFF_T, input->v_offset);

	msg_size = dump_size(input, "msg.size",
		t_strndup(hdr.message_size_hex, sizeof(hdr.message_size_hex)));

	i_stream_skip(input, hdr_size);
	*msg_size_r = msg_size;
	return TRUE;
}

static void dump_msg_metadata(struct istream *input)
{
	struct dbox_metadata_header hdr;
	const unsigned char *data;
	size_t size;
	const char *line;

	/* verify magic */
	if (i_stream_read_bytes(input, &data, &size, sizeof(hdr)) <= 0) {
		i_fatal("dbox missing metadata at %"PRIuUOFF_T,
			input->v_offset);
	}
	memcpy(&hdr, data, sizeof(hdr));
	if (memcmp(hdr.magic_post, DBOX_MAGIC_POST, sizeof(hdr.magic_post)) != 0)
		i_fatal("dbox wrong post-magic at %"PRIuUOFF_T, input->v_offset);
	i_stream_skip(input, sizeof(hdr));

	/* dump the metadata */
	for (;;) {
		if ((line = i_stream_read_next_line(input)) == NULL)
			i_fatal("dbox metadata ended unexpectedly at EOF");
		if (*line == '\0')
			break;

		switch (*line) {
		case DBOX_METADATA_GUID:
			printf("msg.guid = %s\n", line + 1);
			break;
		case DBOX_METADATA_POP3_UIDL:
			printf("msg.pop3-uidl = %s\n", line + 1);
			break;
		case DBOX_METADATA_POP3_ORDER:
			printf("msg.pop3-order = %s\n", line + 1);
			break;
		case DBOX_METADATA_RECEIVED_TIME:
			dump_timestamp(input, "msg.received", line + 1);
			break;
		case DBOX_METADATA_PHYSICAL_SIZE:
			(void)dump_size(input, "msg.physical-size", line + 1);
			break;
		case DBOX_METADATA_VIRTUAL_SIZE:
			(void)dump_size(input, "msg.virtual-size", line + 1);
			break;
		case DBOX_METADATA_EXT_REF:
			printf("msg.ext-ref = %s\n", line + 1);
			break;
		case DBOX_METADATA_ORIG_MAILBOX:
			printf("msg.orig-mailbox = %s\n", line + 1);
			break;

		case DBOX_METADATA_OLDV1_EXPUNGED:
		case DBOX_METADATA_OLDV1_FLAGS:
		case DBOX_METADATA_OLDV1_KEYWORDS:
		case DBOX_METADATA_OLDV1_SAVE_TIME:
		case DBOX_METADATA_OLDV1_SPACE:
			printf("msg.obsolete-%c = %s\n", *line, line + 1);
			break;
		}
	}
}

static bool dump_msg(struct istream *input, unsigned int hdr_size)
{
	uoff_t msg_size;

	if (!dump_msg_hdr(input, hdr_size, &msg_size))
		return FALSE;
	i_stream_skip(input, msg_size);
	dump_msg_metadata(input);
	return TRUE;
}

static void cmd_dump_dbox(struct doveadm_cmd_context *cctx ATTR_UNUSED,
			  const char *path, const char *const *args ATTR_UNUSED)
{
	struct istream *input;
	int fd;
	unsigned int hdr_size;
	bool ret;

	fd = open(path, O_RDONLY);
	if (fd < 0)
		i_fatal("open(%s) failed: %m", path);

	input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
	i_stream_set_name(input, path);
	hdr_size = dump_file_hdr(input);
	do {
		printf("\n");
		T_BEGIN {
			ret = dump_msg(input, hdr_size);
		} T_END;
	} while (ret);
	i_stream_destroy(&input);
}

static bool test_dump_dbox(struct doveadm_cmd_context *cctx ATTR_UNUSED,
			   const char *path)
{
	const char *p;

	p = strrchr(path, '/');
	if (p == NULL)
		p = path;
	else
		p++;
	return str_begins_with(p, "m.") || str_begins_with(p, "u.");
}

struct doveadm_cmd_dump doveadm_cmd_dump_dbox = {
	"dbox",
	test_dump_dbox,
	cmd_dump_dbox
};
