/*
 * Copyright (C) 2013 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

/**
 * @file
 *
 * EFI file protocols
 *
 */

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <wchar.h>
#include <ipxe/image.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/Protocol/SimpleFileSystem.h>
#include <ipxe/efi/Protocol/BlockIo.h>
#include <ipxe/efi/Guid/FileInfo.h>
#include <ipxe/efi/Guid/FileSystemInfo.h>
#include <ipxe/efi/efi_strings.h>
#include <ipxe/efi/efi_file.h>

/** EFI simple file system protocol GUID */
static EFI_GUID efi_simple_file_system_protocol_guid
	= EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;

/** EFI file information GUID */
static EFI_GUID efi_file_info_id = EFI_FILE_INFO_ID;

/** EFI file system information GUID */
static EFI_GUID efi_file_system_info_id = EFI_FILE_SYSTEM_INFO_ID;

/** EFI block I/O protocol GUID */
static EFI_GUID efi_block_io_protocol_guid
	= EFI_BLOCK_IO_PROTOCOL_GUID;

/** EFI media ID */
#define EFI_MEDIA_ID_MAGIC 0x69505845

/** An image exposed as an EFI file */
struct efi_file {
	/** EFI file protocol */
	EFI_FILE_PROTOCOL file;
	/** Image */
	struct image *image;
	/** Current file position */
	size_t pos;
};

static struct efi_file efi_file_root;

/**
 * Get EFI file name (for debugging)
 *
 * @v file		EFI file
 * @ret name		Name
 */
static const char * efi_file_name ( struct efi_file *file ) {

	return ( file->image ? file->image->name : "<root>" );
}

/**
 * Open file
 *
 * @v this		EFI file
 * @ret new		New EFI file
 * @v wname		Filename
 * @v mode		File mode
 * @v attributes	File attributes (for newly-created files)
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_file_open ( EFI_FILE_PROTOCOL *this, EFI_FILE_PROTOCOL **new,
		CHAR16 *wname, UINT64 mode __unused,
		UINT64 attributes __unused ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );
	char name[ wcslen ( wname ) + 1 /* NUL */ ];
	struct efi_file *new_file;
	struct image *image;

	/* Initial '\' indicates opening from the root directory */
	while ( *wname == L'\\' ) {
		file = &efi_file_root;
		wname++;
	}

	/* Allow root directory itself to be opened */
	if ( ( wname[0] == L'\0' ) || ( wname[0] == L'.' ) ) {
		*new = &efi_file_root.file;
		return 0;
	}

	/* Fail unless opening from the root */
	if ( file->image ) {
		DBGC ( file, "EFIFILE %s is not a directory\n",
		       efi_file_name ( file ) );
		return EFI_NOT_FOUND;
	}

	/* Identify image */
	snprintf ( name, sizeof ( name ), "%ls", wname );
	image = find_image ( name );
	if ( ! image ) {
		DBGC ( file, "EFIFILE \"%s\" does not exist\n", name );
		return EFI_NOT_FOUND;
	}

	/* Fail unless opening read-only */
	if ( mode != EFI_FILE_MODE_READ ) {
		DBGC ( file, "EFIFILE %s cannot be opened in mode %#08llx\n",
		       image->name, mode );
		return EFI_WRITE_PROTECTED;
	}

	/* Allocate and initialise file */
	new_file = zalloc ( sizeof ( *new_file ) );
	memcpy ( &new_file->file, &efi_file_root.file,
		 sizeof ( new_file->file ) );
	new_file->image = image_get ( image );
	*new = &new_file->file;
	DBGC ( new_file, "EFIFILE %s opened\n", efi_file_name ( new_file ) );

	return 0;
}

/**
 * Close file
 *
 * @v this		EFI file
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_close ( EFI_FILE_PROTOCOL *this ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	/* Do nothing if this is the root */
	if ( ! file->image )
		return 0;

	/* Close file */
	DBGC ( file, "EFIFILE %s closed\n", efi_file_name ( file ) );
	image_put ( file->image );
	free ( file );

	return 0;
}

/**
 * Close and delete file
 *
 * @v this		EFI file
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_delete ( EFI_FILE_PROTOCOL *this ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	DBGC ( file, "EFIFILE %s cannot be deleted\n", efi_file_name ( file ) );

	/* Close file */
	efi_file_close ( this );

	/* Warn of failure to delete */
	return EFI_WARN_DELETE_FAILURE;
}

/**
 * Return variable-length data structure
 *
 * @v base		Base data structure (starting with UINT64)
 * @v base_len		Length of base data structure
 * @v name		Name to append to base data structure
 * @v len		Length of data buffer
 * @v data		Data buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS efi_file_varlen ( UINT64 *base, size_t base_len,
				    const char *name, UINTN *len, VOID *data ) {
	size_t name_len;

	/* Calculate structure length */
	name_len = strlen ( name );
	*base = ( base_len + ( name_len + 1 /* NUL */ ) * sizeof ( wchar_t ) );
	if ( *len < *base ) {
		*len = *base;
		return EFI_BUFFER_TOO_SMALL;
	}

	/* Copy data to buffer */
	*len = *base;
	memcpy ( data, base, base_len );
	efi_snprintf ( ( data + base_len ), ( name_len + 1 /* NUL */ ),
		       "%s", name );

	return 0;
}

/**
 * Return file information structure
 *
 * @v image		Image, or NULL for the root directory
 * @v len		Length of data buffer
 * @v data		Data buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS efi_file_info ( struct image *image, UINTN *len,
				  VOID *data ) {
	EFI_FILE_INFO info;
	const char *name;

	/* Populate file information */
	memset ( &info, 0, sizeof ( info ) );
	if ( image ) {
		info.FileSize = image->len;
		info.PhysicalSize = image->len;
		info.Attribute = EFI_FILE_READ_ONLY;
		name = image->name;
	} else {
		info.Attribute = ( EFI_FILE_READ_ONLY | EFI_FILE_DIRECTORY );
		name = "";
	}

	return efi_file_varlen ( &info.Size, SIZE_OF_EFI_FILE_INFO, name,
				 len, data );
}

/**
 * Read directory entry
 *
 * @v file		EFI file
 * @v len		Length to read
 * @v data		Data buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS efi_file_read_dir ( struct efi_file *file, UINTN *len,
				      VOID *data ) {
	EFI_STATUS efirc;
	struct image *image;
	unsigned int index;

	/* Construct directory entry at current position */
	index = file->pos;
	for_each_image ( image ) {
		if ( index-- == 0 ) {
			efirc = efi_file_info ( image, len, data );
			if ( efirc == 0 )
				file->pos++;
			return efirc;
		}
	}

	/* No more entries */
	*len = 0;
	return 0;
}

/**
 * Read from file
 *
 * @v this		EFI file
 * @v len		Length to read
 * @v data		Data buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_read ( EFI_FILE_PROTOCOL *this,
					 UINTN *len, VOID *data ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );
	size_t remaining;

	/* If this is the root directory, then construct a directory entry */
	if ( ! file->image )
		return efi_file_read_dir ( file, len, data );

	/* Read from the file */
	remaining = ( file->image->len - file->pos );
	if ( *len > remaining )
		*len = remaining;
	DBGC ( file, "EFIFILE %s read [%#08zx,%#08zx)\n",
	       efi_file_name ( file ), file->pos,
	       ( ( size_t ) ( file->pos + *len ) ) );
	copy_from_user ( data, file->image->data, file->pos, *len );
	file->pos += *len;
	return 0;
}

/**
 * Write to file
 *
 * @v this		EFI file
 * @v len		Length to write
 * @v data		Data buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_write ( EFI_FILE_PROTOCOL *this,
					  UINTN *len, VOID *data __unused ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	DBGC ( file, "EFIFILE %s cannot write [%#08zx, %#08zx)\n",
	       efi_file_name ( file ), file->pos,
	       ( ( size_t ) ( file->pos + *len ) ) );
	return EFI_WRITE_PROTECTED;
}

/**
 * Set file position
 *
 * @v this		EFI file
 * @v position		New file position
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_set_position ( EFI_FILE_PROTOCOL *this,
						 UINT64 position ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	/* If this is the root directory, reset to the start */
	if ( ! file->image ) {
		DBGC ( file, "EFIFILE root directory rewound\n" );
		file->pos = 0;
		return 0;
	}

	/* Check for the magic end-of-file value */
	if ( position == 0xffffffffffffffffULL )
		position = file->image->len;

	/* Fail if we attempt to seek past the end of the file (since
	 * we do not support writes).
	 */
	if ( position > file->image->len ) {
		DBGC ( file, "EFIFILE %s cannot seek to %#08llx of %#08zx\n",
		       efi_file_name ( file ), position, file->image->len );
		return EFI_UNSUPPORTED;
	}

	/* Set position */
	file->pos = position;
	DBGC ( file, "EFIFILE %s position set to %#08zx\n",
	       efi_file_name ( file ), file->pos );

	return 0;
}

/**
 * Get file position
 *
 * @v this		EFI file
 * @ret position	New file position
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_get_position ( EFI_FILE_PROTOCOL *this,
						 UINT64 *position ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	*position = file->pos;
	return 0;
}

/**
 * Get file information
 *
 * @v this		EFI file
 * @v type		Type of information
 * @v len		Buffer size
 * @v data		Buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_get_info ( EFI_FILE_PROTOCOL *this,
					     EFI_GUID *type,
					     UINTN *len, VOID *data ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );
	EFI_FILE_SYSTEM_INFO fsinfo;
	struct image *image;

	/* Determine information to return */
	if ( memcmp ( type, &efi_file_info_id, sizeof ( *type ) ) == 0 ) {

		/* Get file information */
		DBGC ( file, "EFIFILE %s get file information\n",
		       efi_file_name ( file ) );
		return efi_file_info ( file->image, len, data );

	} else if ( memcmp ( type, &efi_file_system_info_id,
			     sizeof ( *type ) ) == 0 ) {

		/* Get file system information */
		DBGC ( file, "EFIFILE %s get file system information\n",
		       efi_file_name ( file ) );
		memset ( &fsinfo, 0, sizeof ( fsinfo ) );
		fsinfo.ReadOnly = 1;
		for_each_image ( image )
			fsinfo.VolumeSize += image->len;
		return efi_file_varlen ( &fsinfo.Size,
					 SIZE_OF_EFI_FILE_SYSTEM_INFO, "iPXE",
					 len, data );
	} else {

		DBGC ( file, "EFIFILE %s cannot get information of type %s\n",
		       efi_file_name ( file ), efi_guid_ntoa ( type ) );
		return EFI_UNSUPPORTED;
	}
}

/**
 * Set file information
 *
 * @v this		EFI file
 * @v type		Type of information
 * @v len		Buffer size
 * @v data		Buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_file_set_info ( EFI_FILE_PROTOCOL *this, EFI_GUID *type,
		    UINTN len __unused, VOID *data __unused ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	DBGC ( file, "EFIFILE %s cannot set information of type %s\n",
	       efi_file_name ( file ), efi_guid_ntoa ( type ) );
	return EFI_WRITE_PROTECTED;
}

/**
 * Flush file modified data
 *
 * @v this		EFI file
 * @v type		Type of information
 * @v len		Buffer size
 * @v data		Buffer
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI efi_file_flush ( EFI_FILE_PROTOCOL *this ) {
	struct efi_file *file = container_of ( this, struct efi_file, file );

	DBGC ( file, "EFIFILE %s flushed\n", efi_file_name ( file ) );
	return 0;
}

/** Root directory */
static struct efi_file efi_file_root = {
	.file = {
		.Revision = EFI_FILE_PROTOCOL_REVISION,
		.Open = efi_file_open,
		.Close = efi_file_close,
		.Delete = efi_file_delete,
		.Read = efi_file_read,
		.Write = efi_file_write,
		.GetPosition = efi_file_get_position,
		.SetPosition = efi_file_set_position,
		.GetInfo = efi_file_get_info,
		.SetInfo = efi_file_set_info,
		.Flush = efi_file_flush,
	},
	.image = NULL,
};

/**
 * Open root directory
 *
 * @v filesystem	EFI simple file system
 * @ret file		EFI file handle
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_file_open_volume ( EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *filesystem __unused,
		       EFI_FILE_PROTOCOL **file ) {

	*file = &efi_file_root.file;
	return 0;
}

/** EFI simple file system protocol */
static EFI_SIMPLE_FILE_SYSTEM_PROTOCOL efi_simple_file_system_protocol = {
	.Revision = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_REVISION,
	.OpenVolume = efi_file_open_volume,
};

/** Dummy block I/O reset */
static EFI_STATUS EFIAPI
efi_block_io_reset ( EFI_BLOCK_IO_PROTOCOL *this __unused,
		     BOOLEAN extended __unused ) {
	return 0;
}

/** Dummy block I/O read */
static EFI_STATUS EFIAPI
efi_block_io_read_blocks ( EFI_BLOCK_IO_PROTOCOL *this __unused,
			   UINT32 MediaId __unused, EFI_LBA lba __unused,
			   UINTN len __unused, VOID *data __unused ) {
	return EFI_DEVICE_ERROR;
}

/** Dummy block I/O write */
static EFI_STATUS EFIAPI
efi_block_io_write_blocks ( EFI_BLOCK_IO_PROTOCOL *this __unused,
			    UINT32 MediaId __unused, EFI_LBA lba __unused,
			    UINTN len __unused, VOID *data __unused ) {
	return EFI_DEVICE_ERROR;
}

/** Dummy block I/O flush */
static EFI_STATUS EFIAPI
efi_block_io_flush_blocks ( EFI_BLOCK_IO_PROTOCOL *this __unused ) {
	return 0;
}

/** Dummy block I/O media */
static EFI_BLOCK_IO_MEDIA efi_block_io_media = {
	.MediaId = EFI_MEDIA_ID_MAGIC,
	.MediaPresent = 1,
	.ReadOnly = 1,
	.BlockSize = 1,
};

/** Dummy EFI block I/O protocol */
static EFI_BLOCK_IO_PROTOCOL efi_block_io_protocol = {
	.Revision = EFI_BLOCK_IO_PROTOCOL_REVISION,
	.Media = &efi_block_io_media,
	.Reset = efi_block_io_reset,
	.ReadBlocks = efi_block_io_read_blocks,
	.WriteBlocks = efi_block_io_write_blocks,
	.FlushBlocks = efi_block_io_flush_blocks,
};

/**
 * Install EFI simple file system protocol
 *
 * @v handle		EFI handle
 * @ret rc		Return status code
 */
int efi_file_install ( EFI_HANDLE *handle ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_STATUS efirc;
	int rc;

	/* Install the simple file system protocol and the block I/O
	 * protocol.  We don't have a block device, but large parts of
	 * the EDK2 codebase make the assumption that file systems are
	 * normally attached to block devices, and so we create a
	 * dummy block device on the same handle just to keep things
	 * looking normal.
	 */
	if ( ( efirc = bs->InstallMultipleProtocolInterfaces (
			handle,
			&efi_block_io_protocol_guid,
			&efi_block_io_protocol,
			&efi_simple_file_system_protocol_guid,
			&efi_simple_file_system_protocol, NULL ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( handle, "Could not install simple file system protocol: "
		       "%s\n", strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Uninstall EFI simple file system protocol
 *
 * @v handle		EFI handle
 */
void efi_file_uninstall ( EFI_HANDLE handle ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;

	/* We must install the file system protocol first, since
	 * otherwise the EDK2 code will attempt to helpfully uninstall
	 * it when the block I/O protocol is uninstalled, leading to a
	 * system lock-up.
	 */
	bs->UninstallMultipleProtocolInterfaces (
			handle,
			&efi_simple_file_system_protocol_guid,
			&efi_simple_file_system_protocol,
			&efi_block_io_protocol_guid,
			&efi_block_io_protocol, NULL );
}
