/*
 *
 *  Copyright (c) 2008-2011 Erich Hoover
 *  Copyright (c) 2015 Timothy Pearson <kb9vqf@pearsoncomputing.net>
 *
 *  libr libbfd Backend - Add resources into ELF binaries using libbfd
 * 
 * *** PLEASE READ THE README FILE FOR LICENSE DETAILS SPECIFIC TO THIS FILE *** 
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * To provide feedback, report bugs, or otherwise contact me:
 * ehoover at mines dot edu
 *
 */

/* Include compile-time parameters */
#include "config.h"

#include "libr.h"
#include "libr-internal.h"

/* File access */
#include <fcntl.h>

/* Safe rename requires some errno() knowledge */
#include <errno.h>

#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

/*
 * Build the libr_file handle for processing with libbfd
 */
libr_intstatus open_handles(libr_file *file_handle, char *filename, libr_access_t access)
{
	bfd *handle = NULL;
	
	handle = bfd_openr(filename, "default");
	if(!handle)
		RETURN(LIBR_ERROR_OPENFAILED, "Failed to open input file");
	if(!bfd_check_format(handle, bfd_object))
		RETURN(LIBR_ERROR_WRONGFORMAT, "Invalid input file format: not a libbfd object");
	if(bfd_get_flavour(handle) != bfd_target_elf_flavour)
		RETURN(LIBR_ERROR_WRONGFORMAT, "Invalid input file format: not an ELF file");
	bfd_set_error(bfd_error_no_error);
	file_handle->filename = filename;
	file_handle->bfd_read = handle;
	file_handle->access = access;
	if(access == LIBR_READ_WRITE)
	{
		struct stat file_stat;
		int fd;
		
		/* Check for write permission on the file */
		fd = open(filename, O_WRONLY);
		if(fd == ERROR)
			RETURN(LIBR_ERROR_WRITEPERM, "No write permission for file");
		close(fd);
		/* Obtain the access mode of the input file */
		if(stat(filename, &file_stat) == ERROR)
			RETURN(LIBR_ERROR_NOSIZE, "Failed to obtain file size");
		file_handle->filemode = file_stat.st_mode;
		file_handle->fileowner = file_stat.st_uid;
		file_handle->filegroup = file_stat.st_gid;
		/* Open a temporary file with the same settings as the input file */
		strcpy(file_handle->tempfile, LIBR_TEMPFILE);
		file_handle->fd_handle = mkstemp(file_handle->tempfile);
		handle = bfd_openw(file_handle->tempfile, bfd_get_target(file_handle->bfd_read));
		if(!bfd_set_format(handle, bfd_get_format(file_handle->bfd_read)))
			RETURN(LIBR_ERROR_SETFORMAT, "Failed to set output file format to input file format");
		if(!bfd_set_arch_mach(handle, bfd_get_arch(file_handle->bfd_read), bfd_get_mach(file_handle->bfd_read)))
			RETURN(LIBR_ERROR_SETARCH, "Failed to set output file architecture to input file architecture");
		/* twice needed ? */
		if(!bfd_set_format(handle, bfd_get_format(file_handle->bfd_read)))
			RETURN(LIBR_ERROR_SETFORMAT, "Failed to set output file format to input file format");
		file_handle->bfd_write = handle;
	}
	else
	{
		file_handle->fd_handle = 0;
		file_handle->bfd_write = NULL;
	} 
	RETURN_OK;
}

/*
 * Check to see if a symbol should be kept
 */
int keep_symbol(libr_section *sections, libr_section *chkscn)
{
	libr_section *scn;
	
	/* Check that the section is publicly exposed */
	for(scn = sections; scn != NULL; scn = scn->next)
	{
		if(scn == chkscn)
		{
			/* if it is, and has size zero, then it was marked for deletion */
			if(bfd_get_section_size(chkscn) == 0)
				return false;
			return true;
		}
	}
	return true;
}

/*
 * Remove the symbol corresponding to a deleted section
 */
void remove_sections(libr_section *sections, void *symtab_buffer, long *symtab_count)
{ 
	asymbol **symtab = (asymbol **) symtab_buffer;
	long i, cnt = *symtab_count;
	
	for(i=0;i<cnt;i++)
	{
		libr_section *chkscn = NULL;
		asymbol *symbol = symtab[i];
		
		if(symbol != NULL)
			chkscn = bfd_get_section(symbol);
		if(chkscn != NULL && !keep_symbol(sections, chkscn))
		{
			/* remove the symbol from the table */
			asymbol **tmp = (asymbol **) malloc(sizeof(asymbol *) * (cnt-(i+1)));
			memcpy(&tmp[0], &symtab[i+1], sizeof(asymbol *) * (cnt-(i+1)));
			memcpy(&symtab[i], &tmp[0], sizeof(asymbol *) * (cnt-(i+1)));
			free(tmp);
			cnt--;
		}
	}
	*symtab_count = cnt;
}

int setup_sections(bfd *ihandle, bfd *ohandle)
{
	libr_section *iscn, *oscn;
	bfd_vma vma;
	
	for(iscn = ihandle->sections; iscn != NULL; iscn = iscn->next)
	{
		if(bfd_get_section_size(iscn) == 0)
		{
			continue; /* Section has been marked for deletion */
		}
		/* Use SEC_LINKER_CREATED to ask the libbfd backend to take care of configuring the section */

		// Keep the ARM_ATTRIBUTES section type intact on armhf systems
		// If this is not done, readelf -A will not print any architecture information for the modified library,
		// and ldd will report that the library cannot be found
		if (strcmp(iscn->name, ".ARM.attributes") == 0)
		{
			oscn = bfd_make_section_anyway_with_flags(ohandle, iscn->name, iscn->flags);
		}
		else
		{
			oscn = bfd_make_section_anyway_with_flags(ohandle, iscn->name, iscn->flags | SEC_LINKER_CREATED);
		}
		if(oscn == NULL)
		{
			printf("failed to create out section: %s\n", bfd_errmsg(bfd_get_error()));
			return false;
		}
		if(!bfd_set_section_size(ohandle, oscn, iscn->size))
		{
			printf("failed to set data size: %s\n", bfd_errmsg(bfd_get_error()));
			return false;
		}
		vma = bfd_section_vma(ihandle, iscn);
		if(!bfd_set_section_vma(ohandle, oscn, vma))
		{
			printf("failed to set virtual memory address: %s\n", bfd_errmsg(bfd_get_error()));
			return false;
		}
		oscn->lma = iscn->lma;
		if(!bfd_set_section_alignment(ohandle, oscn, bfd_section_alignment(ihandle, iscn)))
		{
			printf("failed to compute section alignment: %s\n", bfd_errmsg(bfd_get_error()));
			return false;
		}
		oscn->entsize = iscn->entsize;
		iscn->output_section = oscn;
		iscn->output_offset = vma;
		if(!bfd_copy_private_section_data(ihandle, iscn, ohandle, oscn))
		{
			printf("failed to compute section alignment: %s\n", bfd_errmsg(bfd_get_error()));
			return false;
		}
	}
	return true;
}

/*
 * Go through the rather complicated process of using libbfd to build the output file
 */
int build_output(libr_file *file_handle)
{
	void *symtab_buffer = NULL, *reloc_buffer = NULL, *buffer = NULL;
	bfd_size_type symtab_size, reloc_size, size;
	bfd *ohandle = file_handle->bfd_write;
	bfd *ihandle = file_handle->bfd_read;
	long symtab_count, reloc_count;
	libr_section *iscn, *oscn;
	
	if(!bfd_set_start_address(ohandle, bfd_get_start_address(ihandle)))
	{
		printf("failed to set start address: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	if(!bfd_set_file_flags(ohandle, bfd_get_file_flags(ihandle)))
	{
		printf("failed to set file flags: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	/* Setup the sections in the output file */
	if(!setup_sections(ihandle, ohandle))
		return false; /* error already printed */
	if(!bfd_copy_private_header_data(ihandle, ohandle))
	{
		printf("failed to copy header: %s\n", bfd_errmsg(bfd_get_error()));
		return false; /* failed to create section */
	}
	/* Get the old symbol table */
	if((bfd_get_file_flags(ihandle) & HAS_SYMS) == 0)
	{
		printf("file has no symbol table: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	symtab_size = bfd_get_symtab_upper_bound(ihandle);
	if((signed)symtab_size < 0)
	{
		printf("failed to get symbol table size: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	symtab_buffer = malloc(symtab_size);
	symtab_count = bfd_canonicalize_symtab(ihandle, symtab_buffer);
	if(symtab_count < 0)
	{
		printf("failed to get symbol table number of entries: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	/* Tweak the symbol table to remove sections that no-longer exist */
	remove_sections(ihandle->sections, symtab_buffer, &symtab_count);
	bfd_set_symtab(ohandle, symtab_buffer, symtab_count);
	/* Actually copy section data */
	for(iscn = ihandle->sections; iscn != NULL; iscn = iscn->next)
	{
		size = bfd_get_section_size(iscn);
		if(size == 0)
			continue; /* Section has been marked for deletion */
		oscn = iscn->output_section;
		reloc_size = bfd_get_reloc_upper_bound(ihandle, iscn);
		if(reloc_size == 0)
			bfd_set_reloc(ohandle, oscn, NULL, 0);
		else
		{
			reloc_buffer = malloc(reloc_size);
			reloc_count = bfd_canonicalize_reloc(ihandle, iscn, reloc_buffer, symtab_buffer);
			bfd_set_reloc(ohandle, oscn, reloc_buffer, reloc_count);
		}

		if(bfd_get_section_flags(ihandle, iscn) & SEC_HAS_CONTENTS)
		{
			/* NOTE: if the section is just being copied then do that, otherwise grab
			 * the user data for the section (stored previously by set_data)
			 */
			if(iscn->userdata == NULL)
			{
				buffer = malloc(size);
				if(!bfd_get_section_contents(ihandle, iscn, buffer, 0, size))
				{
					printf("failed to get section contents: %s\n", bfd_errmsg(bfd_get_error()));
					free(buffer);
					return false;
				}
			}
			else
				buffer = iscn->userdata;
			if(!bfd_set_section_contents(ohandle, oscn, buffer, 0, size))
			{
				printf("failed to set section contents: %s\n", bfd_errmsg(bfd_get_error()));
				free(buffer);
				return false;
			}
			free(buffer);
			if(!bfd_copy_private_section_data(ihandle, iscn, ohandle, oscn))
			{
				printf("failed to copy private section data: %s\n", bfd_errmsg(bfd_get_error()));
				return false;
			}
		}
	}
	if(!bfd_copy_private_bfd_data(ihandle, ohandle))
	{
		printf("failed to copy private data: %s\n", bfd_errmsg(bfd_get_error()));
		return false;
	}
	return true;
}

/*
 * Perform a cross-device safe rename
 */
int safe_rename(const char *old, const char *new)
{
	char buffer[1024];
	FILE *in, *out;
	size_t read;
	int status_in;
	int status_out;
	
	in = fopen(old, "r");
	if(!in) {
		return -1;
	}
	out = fopen(new, "w");
	if(!out) {
		fclose(in);
		return -1;
	}
	while(!feof(in) && !ferror(in)) {
		read = fread(buffer, 1, sizeof(buffer), in);
		if (read > 0) {
			fwrite(buffer, 1, read, out);
		}
		if (ferror(in) || ferror(out)) {
			fclose(in);
			fclose(out);
			remove(new);
			return -1;
		}
	}
	status_in = ferror(in);
	status_out = ferror(out);
	fclose(in);
	fclose(out);
	if(status_in || status_out) {
		remove(new);
		return -1;
	}
	return remove(old);
}

/*
 * Write the output file using the libbfd method
 */ 
void write_output(libr_file *file_handle)
{
	int write_ok = false;
	
	if(file_handle->bfd_write != NULL)
	{
		write_ok = true;
		if(!build_output(file_handle))
		{
			printf("failed to build output file.\n");
			write_ok = false;
		}
		if(!bfd_close(file_handle->bfd_write))
		{
			printf("failed to close write handle.\n");
			write_ok = false;
		}
		if(file_handle->fd_handle != 0 && close(file_handle->fd_handle))
		{
			write_ok = false;
			printf("failed to close write file descriptor.\n");
		}
	}
	/* The read handle must be closed last since it is used in the write process */ 
	if(!bfd_close(file_handle->bfd_read))
		printf("failed to close read handle.\n");
	/* Copy the temporary output over the input */
	if(write_ok)
	{
		if(rename(file_handle->tempfile, file_handle->filename) < 0)
		{
			if(errno != EXDEV || safe_rename(file_handle->tempfile, file_handle->filename) < 0)
				printf("failed to rename output file: %m\n");
		}
		if(chmod(file_handle->filename, file_handle->filemode) < 0)
			printf("failed to set file mode.\n");
		if(chown(file_handle->filename, file_handle->fileowner, file_handle->filegroup) < 0)
			printf("failed to set file ownership.\n");
	}
}

/*
 * Find a named section from the ELF file using libbfd
 */
libr_intstatus find_section(libr_file *file_handle, char *section_name, libr_section **retscn)
{
	libr_section *scn;
	
	for(scn = file_handle->bfd_read->sections; scn != NULL; scn = scn->next)
	{
		if(strcmp(scn->name, section_name) == 0)
		{
			*retscn = scn;
			RETURN_OK;
		}
	}
	RETURN(LIBR_ERROR_NOSECTION, "ELF resource section not found");
}

/*
 * Obtain the data from a section using libbfd
 */
libr_data *get_data(libr_file *file_handle, libr_section *scn)
{
	libr_data *data = malloc(scn->size);
	
	if(!bfd_get_section_contents(file_handle->bfd_read, scn, data, 0, scn->size))
	{
		free(data);
		data = NULL;
	}
	scn->userdata = data;
	return data;
}

/*
 * Create new data for a section using libbfd
 */
libr_data *new_data(libr_file *file_handle, libr_section *scn)
{
	/* NOTE: expanding data is handled by set_data for libbfd */
	if(scn->userdata != NULL)
		return scn->userdata;
	scn->size = 0;
	scn->userdata = malloc(0);
	return scn->userdata;
}

/*
 * Create new data for a section using libbfd (at least, do so memory-wise)
 */
libr_intstatus set_data(libr_file *file_handle, libr_section *scn, libr_data *data, off_t offset, char *buffer, size_t size)
{
	char *intbuffer = NULL;
	
	/* special case: clear buffer */
	if(buffer == NULL)
	{
		scn->size = 0;
		if(scn->userdata != NULL)
			free(scn->userdata);
		RETURN_OK;
	}
	/* normal case: add new data to the buffer */
	scn->size = offset + size;
	scn->userdata = realloc(data, scn->size);
	if(scn->userdata == NULL)
		RETURN(LIBR_ERROR_MEMALLOC, "Failed to allocate memory for data");
	intbuffer = scn->userdata;
	memcpy(&intbuffer[offset], buffer, size);
	RETURN_OK;
}

/*
 * Create a new section using libbfd
 */
libr_intstatus add_section(libr_file *file_handle, char *resource_name, libr_section **retscn)
{
	libr_section *scn = NULL;
	
	scn = bfd_make_section(file_handle->bfd_read, resource_name);
	if(scn == NULL)
		RETURN(LIBR_ERROR_NEWSECTION, "Failed to create new section");
	if(!bfd_set_section_flags(file_handle->bfd_read, scn, SEC_HAS_CONTENTS | SEC_DATA | SEC_IN_MEMORY))
		RETURN(LIBR_ERROR_SETFLAGS, "Failed to set flags for section");
	*retscn = scn;
	RETURN_OK;
}

/*
 * Remove a section and eliminate it from the ELF string table using libbfd
 */
libr_intstatus remove_section(libr_file *file_handle, libr_section *scn)
{
	scn->size = 0;
	RETURN_OK;
}

/*
 * Return the pointer to the actual data in the section
 */
void *data_pointer(libr_section *scn, libr_data *data)
{ 
	return data;
}

/*
 * Return the size of the data in the section
 */
size_t data_size(libr_section *scn, libr_data *data)
{
	return scn->size;
}

/*
 * Return the next section in the ELF file
 */
libr_section *next_section(libr_file *file_handle, libr_section *scn)
{
	/* get the first section */
	if(scn == NULL)
	{
		if(file_handle->bfd_read == NULL)
			return NULL;
		return file_handle->bfd_read->sections;
	}
	return scn->next;
}

/*
 * Return the name of a section
 */
char *section_name(libr_file *file_handle, libr_section *scn)
{
	return (char *) scn->name;
}

/*
 * Initialize libbfd
 */
void initialize_backend(void)
{
	bfd_init();
}