/*
 * Copyright 2016-2017 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 */
 
 
#include <string.h>
 
#include <boot/partitions.h>
#include <boot/platform.h>
#include <boot/stage2.h>
#include <boot/stdio.h>
#include <util/list.h>
 
#include "Header.h"
 
#include "efi_platform.h"
#include "efigpt.h"
#include "gpt_known_guids.h"
 
 
struct device_handle {
	list_link			link;
	EFI_DEVICE_PATH*	device_path;
	EFI_HANDLE			handle;
};
 
 
static struct list sMessagingDevices;
static struct list sMediaDevices;
 
static EFI_GUID BlockIoGUID = BLOCK_IO_PROTOCOL;
static EFI_GUID LoadedImageGUID = LOADED_IMAGE_PROTOCOL;
static EFI_GUID DevicePathGUID = DEVICE_PATH_PROTOCOL;
 
 
static UINTN
device_path_length(EFI_DEVICE_PATH* path)
{
	EFI_DEVICE_PATH *node = path;
	UINTN length = 0;
	while (!IsDevicePathEnd(node)) {
		length += DevicePathNodeLength(node);
		node = NextDevicePathNode(node);
	}
 
	// node now points to the device path end node; add its length as well
	return length + DevicePathNodeLength(node);
}
 
 
// If matchSubPath is true, then the second device path can be a sub-path
// of the first device path
static bool
compare_device_paths(EFI_DEVICE_PATH* first, EFI_DEVICE_PATH* second, bool matchSubPath = false)
{
	EFI_DEVICE_PATH *firstNode = first;
	EFI_DEVICE_PATH *secondNode = second;
	while (!IsDevicePathEnd(firstNode) && !IsDevicePathEnd(secondNode)) {
		UINTN firstLength = DevicePathNodeLength(firstNode);
		UINTN secondLength = DevicePathNodeLength(secondNode);
		if (firstLength != secondLength || memcmp(firstNode, secondNode, firstLength) != 0) {
			return false;
		}
		firstNode = NextDevicePathNode(firstNode);
		secondNode = NextDevicePathNode(secondNode);
	}
 
	if (matchSubPath)
		return IsDevicePathEnd(secondNode);
 
	return IsDevicePathEnd(firstNode) && IsDevicePathEnd(secondNode);
}
 
 
static bool
add_device_path(struct list *list, EFI_DEVICE_PATH* path, EFI_HANDLE handle)
{
	device_handle *node = NULL;
	while ((node = (device_handle*)list_get_next_item(list, node)) != NULL) {
		if (compare_device_paths(node->device_path, path))
			return false;
	}
 
	UINTN length = device_path_length(path);
	node = (device_handle*)malloc(sizeof(struct device_handle));
	node->device_path = (EFI_DEVICE_PATH*)malloc(length);
	node->handle = handle;
	memcpy(node->device_path, path, length);
 
	list_add_item(list, node);
 
	return true;
}
 
 
class EfiDevice : public Node
{
	public:
		EfiDevice(EFI_BLOCK_IO *blockIo, EFI_DEVICE_PATH *devicePath);
		virtual ~EfiDevice();
 
		virtual ssize_t ReadAt(void *cookie, off_t pos, void *buffer,
			size_t bufferSize);
		virtual ssize_t WriteAt(void *cookie, off_t pos, const void *buffer,
			size_t bufferSize) { return B_UNSUPPORTED; }
		virtual off_t Size() const {
			return (fBlockIo->Media->LastBlock + 1) * BlockSize(); }
 
		uint32 BlockSize() const { return fBlockIo->Media->BlockSize; }
		bool ReadOnly() const { return fBlockIo->Media->ReadOnly; }
		int32 BootMethod() const {
			if (fDevicePath->Type == MEDIA_DEVICE_PATH) {
				if (fDevicePath->SubType == MEDIA_CDROM_DP)
					return BOOT_METHOD_CD;
				if (fDevicePath->SubType == MEDIA_HARDDRIVE_DP)
					return BOOT_METHOD_HARD_DISK;
			}
 
			return BOOT_METHOD_DEFAULT;
		}
 
		EFI_DEVICE_PATH* DevicePath() { return fDevicePath; }
 
	private:
		EFI_BLOCK_IO*		fBlockIo;
		EFI_DEVICE_PATH*	fDevicePath;
};
 
 
EfiDevice::EfiDevice(EFI_BLOCK_IO *blockIo, EFI_DEVICE_PATH *devicePath)
	:
	fBlockIo(blockIo),
	fDevicePath(devicePath)
{
}
 
 
EfiDevice::~EfiDevice()
{
}
 
 
ssize_t
EfiDevice::ReadAt(void *cookie, off_t pos, void *buffer, size_t bufferSize)
{
	uint32 offset = pos % BlockSize();
	pos /= BlockSize();
 
	uint32 numBlocks = (offset + bufferSize + BlockSize()) / BlockSize();
	char readBuffer[numBlocks * BlockSize()];
 
	if (fBlockIo->ReadBlocks(fBlockIo, fBlockIo->Media->MediaId,
		pos, sizeof(readBuffer), readBuffer) != EFI_SUCCESS)
		return B_ERROR;
 
	memcpy(buffer, readBuffer + offset, bufferSize);
 
	return bufferSize;
}
 
 
static status_t
build_device_handles()
{
	EFI_GUID blockIoGuid = BLOCK_IO_PROTOCOL;
	EFI_GUID devicePathGuid = DEVICE_PATH_PROTOCOL;
 
	EFI_DEVICE_PATH *devicePath, *node;
	EFI_HANDLE *handles = NULL;
	EFI_STATUS status;
	UINTN size = 0;
 
	status = kBootServices->LocateHandle(ByProtocol, &blockIoGuid, 0, &size, 0);
	if (status != EFI_BUFFER_TOO_SMALL)
		return B_ENTRY_NOT_FOUND;
 
	handles = (EFI_HANDLE*)malloc(size);
	status = kBootServices->LocateHandle(ByProtocol, &blockIoGuid, 0, &size,
		handles);
	if (status != EFI_SUCCESS) {
		free(handles);
		return B_ENTRY_NOT_FOUND;
	}
 
	for (UINTN n = 0; n < (size / sizeof(EFI_HANDLE)); n++) {
		status = kBootServices->HandleProtocol(handles[n], &devicePathGuid,
			(void**)&devicePath);
		if (status != EFI_SUCCESS)
			continue;
 
		node = devicePath;
		while (!IsDevicePathEnd(NextDevicePathNode(node)))
			node = NextDevicePathNode(node);
 
		if (DevicePathType(node) == MEDIA_DEVICE_PATH)
			add_device_path(&sMediaDevices, devicePath, handles[n]);
		else if (DevicePathType(node) == MESSAGING_DEVICE_PATH)
			add_device_path(&sMessagingDevices, devicePath, handles[n]);
	}
 
	return B_OK;
}
 
 
static off_t
get_next_check_sum_offset(int32 index, off_t maxSize)
{
	if (index < 2)
		return index * 512;
 
	if (index < 4)
		return (maxSize >> 10) + index * 2048;
 
	return ((system_time() + index) % (maxSize >> 9)) * 512;
}
 
 
static uint32
compute_check_sum(Node *device, off_t offset)
{
	char buffer[512];
	ssize_t bytesRead = device->ReadAt(NULL, offset, buffer, sizeof(buffer));
	if (bytesRead < B_OK)
		return 0;
 
	if (bytesRead < (ssize_t)sizeof(buffer))
		memset(buffer + bytesRead, 0, sizeof(buffer) - bytesRead);
 
	uint32 *array = (uint32*)buffer;
	uint32 sum = 0;
 
	for (uint32 i = 0; i < (bytesRead + sizeof(uint32) - 1) / sizeof(uint32); i++)
		sum += array[i];
 
	return sum;
}
 
 
static device_handle*
get_messaging_device_for_media_device(device_handle *media_device)
{
	device_handle *device = NULL;
	while ((device = (device_handle*)list_get_next_item(&sMessagingDevices,
				device)) != NULL) {
		if (compare_device_paths(media_device->device_path,
				device->device_path, true))
			return device;
	}
 
	return NULL;
}
 
 
static bool
get_boot_uuid(void)
{
	return false;
}
 
 
static status_t
add_boot_device(NodeList *devicesList)
{
	return B_ENTRY_NOT_FOUND;
}
 
 
static status_t
add_boot_device_for_image(NodeList *devicesList)
{
	EFI_LOADED_IMAGE *loadedImage;
	if (kBootServices->HandleProtocol(kImage, &LoadedImageGUID,
			(void**)&loadedImage) != EFI_SUCCESS)
		return B_ERROR;
 
	EFI_DEVICE_PATH *devicePath, *node;
	if (kBootServices->HandleProtocol(loadedImage->DeviceHandle,
			&DevicePathGUID, (void**)&devicePath) != EFI_SUCCESS)
		return B_ERROR;
 
	for (node = devicePath; DevicePathType(node) != MESSAGING_DEVICE_PATH;
			node = NextDevicePathNode(node)) {
		if (IsDevicePathEnd(node))
			return B_ERROR;
	}
 
	SetDevicePathEndNode(NextDevicePathNode(node));
 
	UINTN length = device_path_length(devicePath);
	EFI_DEVICE_PATH *savedDevicePath = (EFI_DEVICE_PATH*)malloc(length);
	memcpy(savedDevicePath, devicePath, length);
 
	EFI_HANDLE handle;
	if (kBootServices->LocateDevicePath(&BlockIoGUID, &devicePath, &handle)
			!= EFI_SUCCESS)
		return B_ERROR;
 
	if (!IsDevicePathEnd(devicePath))
		return B_ERROR;
 
	EFI_BLOCK_IO *blockIo;
	if (kBootServices->HandleProtocol(handle, &BlockIoGUID, (void**)&blockIo)
			!= EFI_SUCCESS)
		return B_ERROR;
 
	if (!blockIo->Media->MediaPresent)
		return B_ERROR;
 
	EfiDevice *device = new(std::nothrow)EfiDevice(blockIo, savedDevicePath);
	if (device == NULL)
		return B_ERROR;
 
	add_device_path(&sMessagingDevices, savedDevicePath, handle);
	devicesList->Insert(device);
 
	return B_OK;
}
 
 
static status_t
add_cd_devices(NodeList *devicesList)
{
	device_handle *handle = NULL;
	while ((handle = (device_handle*)list_get_next_item(&sMediaDevices, handle))
			 != NULL) {
		EFI_DEVICE_PATH *node = handle->device_path;
		while (!IsDevicePathEnd(NextDevicePathNode(node)))
			node = NextDevicePathNode(node);
 
		if (DevicePathType(node) != MEDIA_DEVICE_PATH)
			continue;
 
		if (DevicePathSubType(node) != MEDIA_CDROM_DP)
			continue;
 
		device_handle *messaging_device
			= get_messaging_device_for_media_device(handle);
		if (messaging_device == NULL)
			continue;
 
		EFI_BLOCK_IO *blockIo;
		EFI_GUID blockIoGuid = BLOCK_IO_PROTOCOL;
		EFI_STATUS status = kBootServices->HandleProtocol(messaging_device->handle,
			&blockIoGuid, (void**)&blockIo);
		if (status != EFI_SUCCESS)
			continue;
 
		if (!blockIo->Media->MediaPresent)
			continue;
 
		EfiDevice *device = new(std::nothrow)EfiDevice(blockIo, handle->device_path);
		if (device == NULL)
			continue;
 
		devicesList->Insert(device);
	}
 
	return devicesList->Count() > 0 ? B_OK : B_ENTRY_NOT_FOUND;
}
 
 
static status_t
add_remaining_devices(NodeList *devicesList)
{
	device_handle *node = NULL;
	while ((node = (device_handle*)list_get_next_item(&sMessagingDevices, node)) != NULL) {
		NodeIterator it = devicesList->GetIterator();
		bool found = false;
		while (it.HasNext()) {
			EfiDevice *device = (EfiDevice*)it.Next();
			// device->DevicePath() is a Media Device Path instance
			if (compare_device_paths(device->DevicePath(), node->device_path, true)) {
				found = true;
				break;
			}
		}
 
		if (!found) {
			EFI_BLOCK_IO *blockIo;
			EFI_GUID blockIoGuid = BLOCK_IO_PROTOCOL;
			EFI_STATUS status = kBootServices->HandleProtocol(node->handle,
				&blockIoGuid, (void**)&blockIo);
			if (status != EFI_SUCCESS)
				continue;
 
			if (!blockIo->Media->MediaPresent)
				continue;
 
			EfiDevice *device = new(std::nothrow)EfiDevice(blockIo, node->device_path);
			if (device == NULL)
				continue;
 
			devicesList->Insert(device);
		}
	}
 
	return B_OK;
}
 
 
static bool
device_contains_partition(EfiDevice *device, boot::Partition *partition)
{
	EFI::Header *header = (EFI::Header*)partition->content_cookie;
	if (header != NULL && header->InitCheck() == B_OK) {
		// check if device is GPT, and contains partition entry
		uint32 blockSize = device->BlockSize();
		EFI_PARTITION_TABLE_HEADER *deviceHeader =
			(EFI_PARTITION_TABLE_HEADER*)malloc(blockSize);
		ssize_t bytesRead = device->ReadAt(NULL, blockSize, deviceHeader,
			blockSize);
		if (bytesRead != blockSize)
			return false;
 
		if (memcmp(deviceHeader, &header->TableHeader(),
				sizeof(efi_table_header)) != 0)
			return false;
 
		// partition->cookie == int partition entry index
		uint32 index = (uint32)(addr_t)partition->cookie;
		uint32 size = sizeof(EFI_PARTITION_ENTRY) * (index + 1);
		EFI_PARTITION_ENTRY *entries = (EFI_PARTITION_ENTRY*)malloc(size);
		bytesRead = device->ReadAt(NULL,
			deviceHeader->PartitionEntryLBA * blockSize, entries, size);
		if (bytesRead != size)
			return false;
 
		if (memcmp(&entries[index], &header->EntryAt(index),
				sizeof(efi_partition_entry)) != 0)
			return false;
 
		for (size_t i = 0; i < sizeof(kTypeMap) / sizeof(struct type_map); ++i)
			if (strcmp(kTypeMap[i].type, BFS_NAME) == 0)
				if (kTypeMap[i].guid == header->EntryAt(index).partition_type)
					return true;
 
		// Our partition has an EFI header, but we couldn't find one, so bail
		return false;
	}
 
	if ((partition->offset + partition->size) <= device->Size())
		return true;
 
	return false;
}
 
 
status_t
platform_add_boot_device(struct stage2_args *args, NodeList *devicesList)
{
	// This is the first entry point, so init the lists here
	list_init(&sMessagingDevices);
	list_init(&sMediaDevices);
 
	build_device_handles();
 
	if (get_boot_uuid()) {
		// If we have the UUID, add the boot device containing that partition
		return add_boot_device(devicesList);
	} else {
		// If we don't have a UUID, add all CD devices with media, and the
		// device that haiku_loader.efi is located on
		add_boot_device_for_image(devicesList);
			// We do this first, so that booting from CD is the fallback
		add_cd_devices(devicesList);
		if (devicesList->Count() > 0)
			return B_OK;
	}
 
	// Otherwise, we don't know what the boot device is; defer to
	// platform_add_block_devices() to add the rest
	return B_ENTRY_NOT_FOUND;
}
 
 
status_t
platform_add_block_devices(struct stage2_args *args, NodeList *devicesList)
{
	return add_remaining_devices(devicesList);
}
 
 
status_t
platform_get_boot_partitions(struct stage2_args *args, Node *bootDevice,
		NodeList *partitions, NodeList *bootPartitions)
{
	NodeIterator iterator = partitions->GetIterator();
	boot::Partition *partition = NULL;
	while ((partition = (boot::Partition*)iterator.Next()) != NULL) {
		if (device_contains_partition((EfiDevice*)bootDevice, partition)) {
			iterator.Remove();
			bootPartitions->Insert(partition);
		}
	}
 
	return bootPartitions->Count() > 0 ? B_OK : B_ENTRY_NOT_FOUND;
}
 
 
status_t
platform_register_boot_device(Node *device)
{
	EfiDevice *efiDevice = (EfiDevice *)device;
	disk_identifier identifier;
 
	// TODO: Setup using device path
	identifier.bus_type = UNKNOWN_BUS;
	identifier.device_type = UNKNOWN_DEVICE;
	identifier.device.unknown.size = device->Size();
 
	for (uint32 i = 0; i < NUM_DISK_CHECK_SUMS; ++i) {
		off_t offset = get_next_check_sum_offset(i, device->Size());
		identifier.device.unknown.check_sums[i].offset = offset;
		identifier.device.unknown.check_sums[i].sum = compute_check_sum(device, offset);
	}
 
	gBootVolume.SetInt32(BOOT_METHOD, efiDevice->BootMethod());
	gBootVolume.SetBool(BOOT_VOLUME_BOOTED_FROM_IMAGE, efiDevice->ReadOnly());
	gBootVolume.SetData(BOOT_VOLUME_DISK_IDENTIFIER, B_RAW_TYPE,
		&identifier, sizeof(disk_identifier));
 
	return B_OK;
}
 
 
void
platform_cleanup_devices()
{
}

V773 The function was exited without releasing the 'savedDevicePath' pointer. A memory leak is possible.

V512 A call of the 'memcmp' function will lead to underflow of the buffer 'deviceHeader'.