// SPDX-License-Identifier: BSD-3-Clause
/*
 * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
 */
#include "firehose.h"
#include <stdlib.h>
#include <string.h>
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>

#include "qdl.h"
#include "gpt.h"

struct gpt_guid {
	uint32_t data1;
	uint16_t data2;
	uint16_t data3;
	uint8_t  data4[8];
} __attribute__((packed));

static const struct gpt_guid gpt_zero_guid = {0};

struct gpt_header {
	uint8_t signature[8];
	uint32_t revision;
	uint32_t header_size;
	uint32_t header_crc32;
	uint32_t reserved;
	uint64_t current_lba;
	uint64_t backup_lba;
	uint64_t first_usable_lba;
	uint64_t last_usable_lba;
	struct gpt_guid disk_guid;
	uint64_t part_entry_lba;
	uint32_t num_part_entries;
	uint32_t part_entry_size;
	uint32_t part_array_crc32;
	uint8_t reserved2[420];
} __attribute__((packed));

struct gpt_entry {
	struct gpt_guid type_guid;
	struct gpt_guid unique_guid;
	uint64_t first_lba;
	uint64_t last_lba;
	uint64_t attrs;
	uint16_t name_utf16le[36];
} __attribute__((packed));

struct gpt_partition {
	const char *name;
	unsigned int partition;
	unsigned int start_sector;
	unsigned int num_sectors;

	struct gpt_partition *next;
};

static struct gpt_partition *gpt_partitions;
static struct gpt_partition *gpt_partitions_last;

static void utf16le_to_utf8(uint16_t *in, size_t in_len, uint8_t *out, size_t out_len)
{
	uint32_t codepoint;
	uint16_t high;
	uint16_t low;
	uint16_t w;
	size_t i;
	size_t j = 0;

	for (i = 0; i < in_len; i++) {
		w = in[i];

		if (w >= 0xd800 && w <= 0xdbff) {
			high = w - 0xd800;

			if (i < in_len) {
				w = in[++i];
				if (w >= 0xdc00 && w <= 0xdfff) {
					low = w - 0xdc00;
					codepoint = (((uint32_t)high << 10) | low) + 0x10000;
				} else {
					/* Surrogate without low surrogate */
					codepoint = 0xfffd;
				}
			} else {
				/* Lone high surrogate at end of string */
				codepoint = 0xfffd;
			}
		} else if (w >= 0xdc00 && w <= 0xdfff) {
			/* Low surrogate without high */
			codepoint = 0xfffd;
		} else {
			codepoint = w;
		}

		if (codepoint == 0)
			break;

		if (codepoint <= 0x7f) {
			if (j + 1 >= out_len)
				break;
			out[j++] = (uint8_t)codepoint;
		} else if (codepoint <= 0x7ff) {
			if (j + 2 >= out_len)
				break;
			out[j++] = 0xc0 | ((codepoint >> 6) & 0x1f);
			out[j++] = 0x80 | (codepoint & 0x3f);
		} else if (codepoint <= 0xffff) {
			if (j + 3 >= out_len)
				break;
			out[j++] = 0xe0 | ((codepoint >> 12) & 0x0f);
			out[j++] = 0x80 | ((codepoint >> 6) & 0x3f);
			out[j++] = 0x80 | (codepoint & 0x3f);
		} else if (codepoint <= 0x10ffff) {
			if (j + 4 >= out_len)
				break;
			out[j++] = 0xf0 | ((codepoint >> 18) & 0x07);
			out[j++] = 0x80 | ((codepoint >> 12) & 0x3f);
			out[j++] = 0x80 | ((codepoint >> 6) & 0x3f);
			out[j++] = 0x80 | (codepoint & 0x3f);
		}
	}

	out[j] = '\0';
}

static int gpt_load_table_from_partition(struct qdl_device *qdl, unsigned int phys_partition, bool *eof)
{
	struct gpt_partition *partition;
	struct gpt_entry *entry;
	struct gpt_header gpt;
	uint8_t buf[4096];
	struct firehose_op op;
	unsigned int offset;
	uint64_t lba;
	char lba_buf[21];
	uint16_t name_utf16le[36];
	char name[36 * 4];
	int ret;
	unsigned int i;

	memset(&op, 0, sizeof(op));

	op.type = FIREHOSE_OP_READ;
	op.sector_size = qdl->sector_size;
	op.start_sector = "1";
	op.num_sectors = 1;
	op.partition = phys_partition;

	memset(&buf, 0, sizeof(buf));
	ret = firehose_read_buf(qdl, &op, &gpt, sizeof(gpt));
	if (ret) {
		/* Assume that we're beyond the last partition */
		*eof = true;
		return -1;
	}

	if (memcmp(gpt.signature, "EFI PART", 8)) {
		ux_err("partition %d has not GPT header\n", phys_partition);
		return 0;
	}

	if (gpt.part_entry_size > qdl->sector_size || gpt.num_part_entries > 1024) {
		ux_debug("partition %d has invalid GPT header\n", phys_partition);
		return -1;
	}

	ux_debug("Loading GPT table from physical partition %d\n", phys_partition);
	for (i = 0; i < gpt.num_part_entries; i++) {
		offset = (i * gpt.part_entry_size) % qdl->sector_size;

		if (offset == 0) {
			lba = gpt.part_entry_lba + i * gpt.part_entry_size / qdl->sector_size;
			snprintf(lba_buf, sizeof(lba_buf), "%" PRIu64, lba);
			op.start_sector = lba_buf;

			memset(buf, 0, sizeof(buf));
			ret = firehose_read_buf(qdl, &op, buf, sizeof(buf));
			if (ret) {
				ux_err("failed to read GPT partition entries from %d:%u\n", phys_partition, lba);
				return -1;
			}
		}

		entry = (struct gpt_entry *)(buf + offset);

		if (!memcmp(&entry->type_guid, &gpt_zero_guid, sizeof(struct gpt_guid)))
			continue;

		memcpy(name_utf16le, entry->name_utf16le, sizeof(name_utf16le));
		utf16le_to_utf8(name_utf16le, 36, (uint8_t *)name, sizeof(name));

		partition = calloc(1, sizeof(*partition));
		partition->name = strdup(name);
		partition->partition = phys_partition;
		partition->start_sector = entry->first_lba;
		/* if first_lba == last_lba there is 1 sector worth of data (IE: add 1 below) */
		partition->num_sectors = entry->last_lba - entry->first_lba + 1;

		ux_debug("  %3d: %s start sector %u, num sectors %u\n", i, partition->name,
			 partition->start_sector, partition->num_sectors);

		if (gpt_partitions) {
			gpt_partitions_last->next = partition;
			gpt_partitions_last = partition;
		} else {
			gpt_partitions = partition;
			gpt_partitions_last = partition;
		}
	}

	return 0;
}

static int gpt_load_tables(struct qdl_device *qdl)
{
	unsigned int i;
	bool eof = false;
	int ret = 0;

	if (gpt_partitions)
		return 0;

	for (i = 0; ; i++) {
		ret = gpt_load_table_from_partition(qdl, i, &eof);
		if (ret)
			break;
	}

	return eof ? 0 : ret;
}

int gpt_find_by_name(struct qdl_device *qdl, const char *name, int *phys_partition,
		     unsigned int *start_sector, unsigned int *num_sectors)
{
	struct gpt_partition *gpt_part;
	bool found = false;
	int ret;

	if (qdl->dev_type == QDL_DEVICE_SIM)
		return 0;

	ret = gpt_load_tables(qdl);
	if (ret < 0)
		return -1;

	for (gpt_part = gpt_partitions; gpt_part; gpt_part = gpt_part->next) {
		if (*phys_partition >= 0 && gpt_part->partition != (unsigned int)(*phys_partition))
			continue;

		if (strcmp(gpt_part->name, name))
			continue;

		if (found) {
			ux_err("duplicate candidates for partition \"%s\" found\n", name);
			return -1;
		}

		*phys_partition = gpt_part->partition;
		*start_sector = gpt_part->start_sector;
		*num_sectors = gpt_part->num_sectors;

		found = true;
	}

	if (!found) {
		if (*phys_partition >= 0)
			ux_err("no partition \"%s\" found on physical partition %d\n", name, *phys_partition);
		else
			ux_err("no partition \"%s\" found\n", name);
		return -1;
	}

	return 0;
}

int gpt_resolve_deferrals(struct qdl_device *qdl, struct list_head *ops)
{
	unsigned int start_sector;
	struct firehose_op *op;
	char buf[20];
	int ret;

	list_for_each_entry(op, ops, node) {
		if (op->type != FIREHOSE_OP_PROGRAM &&
		    op->type != FIREHOSE_OP_ERASE &&
		    op->type != FIREHOSE_OP_READ)
			continue;

		if (!op->gpt_partition)
			continue;

		ret = gpt_find_by_name(qdl, op->gpt_partition, &op->partition,
				       &start_sector, &op->num_sectors);
		if (ret < 0)
			return -1;

		sprintf(buf, "%u", start_sector);
		op->start_sector = strdup(buf);
	}

	return 0;
}
