/*
 * XOR File Destroyer v.1 Beta 8
 *
 * AUTHOR:
 *  Robert Fahy <rfahy@ymail.com>
 *
 * DATE:
 *  May 2011 (initial release)
 *
 * ABOUT:
 *  - previous version was never released because it had entered the phase of "development hell",
 *   so I decided to begin anew and write a dumber version with the sole requirement that
 *   it "worked", whilst the process of improving robustness and security would follow
 *   only after a working version was out.
 *
 * TODO:
 *  - remove duplicate code
 *  - verify against using same file for input and output, in cases 2 and 3?
 *  - add wildcard operation?
 *  - add `ferror()' checks?
 *
 */

// standard include files
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// environment specific include files
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <unistd.h>

// preprocessor definitions with obvious purposes
#define NAME_VERSION			"XOR-FD v.1 Beta 8"
#define GENERIC_FILENAME		"<xorcorrupt.elf>"
#define GENERIC_MESSAGE			"<Error text is unavailable.>"

// these are used to handle cleanup after signals
#define XORFD_ABORT				0
#define XORFD_CONTINUE			1

// how much data in bytes will be read or written from or to a file in one iteration
// WARN: must not be zero (0)
// ABOUT: 1 MB
//#define BUFFER_SIZE				1073741824
// ABOUT: 16 KB
#define BUFFER_SIZE				16384

// maximum amount of data to be read from standard input (stdin) stream
// WARN: must not be zero (0)
#define STDIN_BUFFER_SIZE		4096

// ABOUT: according to the manpages this "will turn off_t into a 64-bit type".
//  translation: it allows us to work with larger files
#define _FILE_OFFSET_BITS		64

// evil global variables
char _global_padding_[STDIN_BUFFER_SIZE];
size_t _global_padding_length_ = 0;
volatile sig_atomic_t _global_run_flag_ = XORFD_CONTINUE;

// declarations without definitions (aka prototypes)
void signals_setup(void);
void signal_handler(int signal);
void print_usage(const char *program_filename, const char *message);
unsigned char get_percentage(const off_t full, const off_t current);
off_t get_file_size_tame(FILE *file);
off_t get_file_size(FILE *file);
char * encrypt_static(char *data, size_t data_length);
char * encrypt_normal(char *data, const char *padding, size_t length);
int compute(const char *argument1, const char *argument2, const char *argument3);
int main(int argc, char **argv); // for completeness of this "index"

void signals_setup()
// ABOUT: sets up the signals' "Rules of Engagement"
{
	signal(SIGHUP, signal_handler);
	signal(SIGTERM, signal_handler);
} // ### void signals_setup()

void signal_handler(int signal)
// ABOUT: modifies `_global_run_flag_' so that the program can quit cleanly
{
	switch (signal)
	{
		case SIGHUP:
			fprintf(stderr, "\n* Caught the \"Hangup\" signal (SIGHUP).\n");
			break;

		case SIGTERM:
			fprintf(stderr, "\n* Caught the \"Terminate\" signal (SIGTERM).\n"
				"* Attempting to close files and free memory... ");
			_global_run_flag_ = XORFD_ABORT;
			break;

		default: fprintf(stderr, "\nSomething's rotten in the State of Denmark.\n");
	}
} // ### void signal_handler(int signal)

void print_usage(const char *program_filename, const char *message)
// ABOUT: prints the usage information and `message' to Standard Error stream (stderr)
{
#ifndef TEXT_BUFFER_SIZE
#define TEXT_BUFFER_SIZE	127
#endif

	char
		program_filename_[TEXT_BUFFER_SIZE + 1],
		message_[TEXT_BUFFER_SIZE + 1];

	if (program_filename != NULL &&
		message != NULL)
	{
		strncpy(program_filename_, program_filename, TEXT_BUFFER_SIZE);
		program_filename_[TEXT_BUFFER_SIZE] = '\0';
		strncpy(message_, message, TEXT_BUFFER_SIZE);
		message_[TEXT_BUFFER_SIZE] = '\0';

		fprintf(stderr, "\n\n%s usage:\n\n %s [file_to_be_encrypted] <[resulting_file] [file_used_as_key]>\n"
			"Also accepting standard input (stdin) data as key if `file_used_as_key' isn't provided.\n"
			"(Maximum stdin key length is %d bytes.)\n\nERROR: %s\n\n\n",
			NAME_VERSION, program_filename_, STDIN_BUFFER_SIZE, message_);
	}
} // ### void print_usage(const char *program_filename, const char *message)

unsigned char get_percentage(const off_t full, const off_t current)
// ABOUT: computes how much percent `current' represents from `full'
// NOTE: sanity of parameters not verified, so this can return values of over 100 (%)
{
	return current * 100 / full;
} // ### unsigned char get_percentage(const off_t full, const off_t current)

off_t get_file_size_tame(FILE *file)
// ABOUT: same as `get_file_size()' but doesn't `exit()' on error and doesn't
//  try to close the `file'
// NOTE: this function might not be portable to non-Unix operating systems
{
	int file_descriptor_;
	struct stat file_stat_;

	if ((file_descriptor_ = fileno(file))
		== -1)
	{
		perror("get_file_size_tame() -> fileno(file)");
		return -1;
	}
	else
		if (fstat(file_descriptor_, &file_stat_) == -1)
		{
			perror("get_file_size_tame() -> fstat(file_descriptor_, &file_stat_)");
			return -1;
		}
		else
			return file_stat_.st_size;
} // ### off_t get_file_size_tame(FILE *file)

off_t get_file_size(FILE *file)
// ABOUT: retrieve the file size of `file'
// NOTE: this function might not be portable to non-Unix operating systems
{
	int file_descriptor_;
	struct stat file_stat_;

	if ((file_descriptor_ = fileno(file))
		== -1)
	{
		perror("get_file_size() -> fileno(file)");

		if (fclose(file) == EOF)
			perror("get_file_size() -> fclose(file) #1");

		exit(EXIT_FAILURE);
	}
	else
		if (fstat(file_descriptor_, &file_stat_) == -1)
		{
			perror("get_file_size() -> fstat(file_descriptor_, &file_stat_)");

			if (fclose(file) == EOF)
				perror("get_file_size() -> fclose(file) #2");

			exit(EXIT_FAILURE);
		}
		else
			return file_stat_.st_size;
} // ### off_t get_file_size(FILE *file)

char * encrypt_static(char *data, size_t length)
// ABOUT: uses XOR operation to pad contents of `data' with contents of `_global_padding_' for given `length'
// WARN: this version loops `_global_padding_' and it assumes that it doesn't change.
//  it also takes into account `_global_padding_length_'. this is a design flaw I'm aware of.
{
	static size_t i=0;

	if (data != NULL &&
		_global_padding_ != NULL)
		while (length--)
		{
			if (i >= _global_padding_length_)
			// reset `i' to keep within bounds
				i=0;

			*data++ ^= _global_padding_[i++];
		}
	else
		return NULL;

	return data;
} // ### char * encrypt_static(char *data, size_t data_length)

char * encrypt_normal(char *data, const char *padding, size_t length)
// ABOUT: uses XOR operation to pad contents of `data' with contents of `padding' for given `length'
// WARN: both arrays need to have same `length'
{
	if (data != NULL &&
		padding != NULL)
		while (length--)
			*data++ ^= *padding++;
	else
		return NULL;

	return data;
} // ### char * encrypt_normal(char *data, const char *padding, size_t length)

int compute(const char *argument1, const char *argument2, const char *argument3)
// ABOUT: verifies the arguments and takes the correct course of action
{
	FILE // file streams
		*input_file_ = NULL,
		*output_file_ = NULL,
		*key_file_ = NULL;

	char
		rw_buffer_[BUFFER_SIZE],	// read/write file buffer
		key_buffer_[BUFFER_SIZE];	// key file buffer

	off_t // to keep track of the total number of bytes we read or wrote
		total_bytes_read_ = 0,
		total_bytes_written_ = 0,
		input_file_size_ = 0; // needed for percentage calculus

	size_t // to keep track of how many bytes we read or wrote
		rw_length_ = 0,
		key_length_ = 0,
		buffer_free_ = BUFFER_SIZE; // how much of the buffer is free (needed for case 3)

	if (argument1 == NULL)
	{
		print_usage(GENERIC_FILENAME, "Nice try.");
		return EXIT_FAILURE;
	}
	else
		if (argument2 == NULL)
		{
			if (argument3 == NULL)	// NOTE: case 1: input file overwritten, key from standard input
			{
				if ((input_file_ = fopen(argument1, "r+b"))
					== NULL)
				{
					perror("compute() -> input_file_ = fopen(argument1, \"r+b\")");
					return EXIT_FAILURE;
				}
				else
				{
					input_file_size_ = get_file_size(input_file_);

					// NOTE: we don't check this for errors because the program will jam waiting anyway,
					//  if no data is fed through standard input (stdin)
					_global_padding_length_ = fread(_global_padding_, sizeof (char), STDIN_BUFFER_SIZE, stdin);
					printf("\n\n%s\n\n* Input/Output file:\t%s\n* Progress:\t  0%%", NAME_VERSION, argument1);

					while ((rw_length_ = fread(rw_buffer_, sizeof (char), BUFFER_SIZE, input_file_)) &&
						_global_run_flag_ == XORFD_CONTINUE)
					// did we read something? yes...
					{
						total_bytes_read_ += rw_length_;

						if (encrypt_static(rw_buffer_, rw_length_) == NULL)
						{
							fprintf(stderr, "\nFailure (case 1): compute() -> encrypt_static(rw_buffer_, rw_length_)\n");

							if (fclose(input_file_) == EOF)
								perror("compute() -> fclose(input_file_) #1");

							return EXIT_FAILURE;
						}
						else
							if (fseeko(input_file_, (off_t)-rw_length_, SEEK_CUR) == -1)
							{
								perror("compute() -> fseeko(input_file_, (off_t)-rw_length_, SEEK_CUR)");

								if (fclose(input_file_) == EOF)
									perror("compute() -> fclose(input_file_) #2");

								return EXIT_FAILURE;
							}
							else
							{
								total_bytes_written_ += fwrite(rw_buffer_, sizeof (char), rw_length_, input_file_);

								if (ferror(input_file_))
								{
									fprintf(stderr, "\nFailure (case 1): compute() -> ferror(input_file_)\n");

									if (fclose(input_file_) == EOF)
										perror("compute() -> fclose(input_file_) #17");

									return EXIT_FAILURE;
								}
								else
									if (fflush(input_file_) == EOF)
									{
										perror("compute() -> fflush(input_file_)");

										if (fclose(input_file_) == EOF)
											perror("compute() -> fclose(input_file_) #3");

										return EXIT_FAILURE;
									}
									else
										printf("\b\b\b\b%3d%%", get_percentage(input_file_size_, total_bytes_written_));
							}
					}
				}

				if (fclose(input_file_) == EOF)
					perror("compute() -> fclose(input_file_) #4");
			}
			else // argument2 is NULL, whilst argument3 isn't
			{
				print_usage(GENERIC_FILENAME, "Nice try.");
				return EXIT_FAILURE;
			}
		}
		else
			if (argument3 == NULL)	// NOTE: case 2: input file, output file, key from standard input
				if ((input_file_ = fopen(argument1, "rb"))
					== NULL)
				{
					perror("case 2: compute() -> input_file_ = fopen(argument1, \"rb\")");
					return EXIT_FAILURE;
				}
				else
				{
					input_file_size_ = get_file_size(input_file_);

					if ((output_file_ = fopen(argument2, "wb"))
						== NULL)
					{
						perror("case 2: compute() -> output_file_ = fopen(argument2, \"wb\")");

						if (fclose(input_file_) == EOF)
							perror("compute() -> fclose(input_file_) #5");

						return EXIT_FAILURE;
					}
					else
					{
						// NOTE: we don't check this for errors because the program will jam waiting anyway,
						//  if no data is fed through standard input (stdin)
						_global_padding_length_ = fread(_global_padding_, sizeof (char), STDIN_BUFFER_SIZE, stdin);
						printf("\n\n%s\n\n* Input file:\t%s\n* Output file:\t%s\n* Progress:\t  0%%", NAME_VERSION, argument1, argument2);

						while (!feof(input_file_) &&
							_global_run_flag_ == XORFD_CONTINUE)
						{
							total_bytes_read_ += (rw_length_ = fread(rw_buffer_, sizeof (char), BUFFER_SIZE, input_file_));

							if (ferror(input_file_))
							{
								fprintf(stderr, "\nFailure (case 2): compute() -> ferror(input_file_)\n");

								if (fclose(input_file_) == EOF)
									perror("compute() -> fclose(input_file_) #16");

								if (fclose(output_file_) == EOF)
									perror("compute() -> fclose(output_file_) #10");

								return EXIT_FAILURE;
							}
							else
								if (encrypt_static(rw_buffer_, rw_length_) == NULL)
								{
									fprintf(stderr, "\nFailure (case 2): compute() -> encrypt_static(rw_buffer_, rw_length_)\n");

									if (fclose(input_file_) == EOF)
										perror("compute() -> fclose(input_file_) #6");

									if (fclose(output_file_) == EOF)
										perror("compute() -> fclose(output_file_) #1");

									return EXIT_FAILURE;
								}
								else
								{
									total_bytes_written_ += fwrite(rw_buffer_, sizeof (char), rw_length_, output_file_);

									if (ferror(output_file_))
									{
										fprintf(stderr, "\nFailure (case 2): compute() -> ferror(output_file_)\n");

										if (fclose(input_file_) == EOF)
											perror("compute() -> fclose(input_file_) #15");

										if (fclose(output_file_) == EOF)
											perror("compute() -> fclose(output_file_) #9");

										return EXIT_FAILURE;
									}
									else
										printf("\b\b\b\b%3d%%", get_percentage(input_file_size_, total_bytes_written_));
								}
						}

						if (fclose(input_file_) == EOF)
							perror("compute() -> fclose(input_file_) #7");

						if (fclose(output_file_) == EOF)
							perror("compute() -> fclose(output_file_) #2");
					}
				}
			else	// NOTE: case 3: input file, output file, key file
				if ((input_file_ = fopen(argument1, "rb"))
					== NULL)
				{
					perror("case 3: compute() -> input_file_ = fopen(argument1, \"rb\")");
					return EXIT_FAILURE;
				}
				else
				{
					input_file_size_ = get_file_size(input_file_);

					if ((output_file_ = fopen(argument2, "wb"))
						== NULL)
					{
						perror("case 3: compute() -> output_file_ = fopen(argument2, \"wb\")");

						if (fclose(input_file_) == EOF)
							perror("compute() -> fclose(input_file_) #8");

						return EXIT_FAILURE;
					}
					else
						if ((key_file_ = fopen(argument3, "rb"))
							== NULL)
						{
							perror("case 3: compute() -> key_file_ = fopen(argument3, \"rb\")");

							if (fclose(input_file_) == EOF)
								perror("compute() -> fclose(input_file_) #9");

							if (fclose(output_file_) == EOF)
								perror("compute() -> fclose(output_file_) #3");

							return EXIT_FAILURE;
						}
						else
							if (get_file_size_tame(key_file_) <= 0)
							// we either have error (-1 return), or file size is zero bytes (0 return)
							{
								print_usage(GENERIC_FILENAME, "The key file is either empty (0 bytes) or cannot be read.");

								if (fclose(input_file_) == EOF)
									perror("compute() -> fclose(input_file_) #12");

								if (fclose(output_file_) == EOF)
									perror("compute() -> fclose(output_file_) #6");

								if (fclose(key_file_) == EOF)
									perror("compute() -> fclose(output_file_) #3");

								return EXIT_FAILURE;
							}
							else
							{
								printf("\n\n%s\n\n* Input file:\t%s\n* Output file:\t%s\n* Key file:\t%s\n* Progress:\t  0%%",
									NAME_VERSION, argument1, argument2, argument3);

								while (!feof(input_file_) &&
									_global_run_flag_ == XORFD_CONTINUE)
								{
									key_length_ = 0;
									total_bytes_read_ += buffer_free_ = rw_length_ = fread(rw_buffer_, sizeof (char), BUFFER_SIZE, input_file_);

									if (ferror(input_file_))
									{
										fprintf(stderr, "\nFailure (case 3): compute() -> ferror(input_file_)\n");

										if (fclose(input_file_) == EOF)
											perror("compute() -> fclose(input_file_) #14");

										if (fclose(output_file_) == EOF)
											perror("compute() -> fclose(output_file_) #8");

										if (fclose(key_file_) == EOF)
											perror("compute() -> fclose(key_file_) #5");

										return EXIT_FAILURE;
									}
									else
									{
										while (key_length_ < rw_length_)
										{
											key_length_ += fread(key_buffer_ + key_length_, sizeof (char), buffer_free_, key_file_);

											if (ferror(key_file_))
											{
												fprintf(stderr, "\nFailure (case 3): compute() -> ferror(key_file_)\n");

												if (fclose(input_file_) == EOF)
													perror("compute() -> fclose(input_file_) #13");

												if (fclose(output_file_) == EOF)
													perror("compute() -> fclose(output_file_) #7");

												if (fclose(key_file_) == EOF)
													perror("compute() -> fclose(key_file_) #4");

												return EXIT_FAILURE;
											}
											else
											{
												buffer_free_ = rw_length_ - key_length_;

												if (feof(key_file_))
													rewind(key_file_);
											}
										}

										if (encrypt_normal(rw_buffer_, key_buffer_, rw_length_) == NULL)
										{
											fprintf(stderr, "\nFailure (case 3): compute() -> encrypt_normal(rw_buffer_, key_buffer_, rw_length_)\n");

											if (fclose(input_file_) == EOF)
												perror("compute() -> fclose(input_file_) #10");

											if (fclose(output_file_) == EOF)
												perror("compute() -> fclose(output_file_) #4");

											if (fclose(key_file_) == EOF)
												perror("compute() -> fclose(key_file_) #1");

											return EXIT_FAILURE;
										}
										else
										{
											total_bytes_written_ += fwrite(rw_buffer_, sizeof (char), rw_length_, output_file_);

											if (ferror(output_file_))
											{
												fprintf(stderr, "\nFailure (case 3): compute() -> ferror(output_file_)\n");

												if (fclose(input_file_) == EOF)
													perror("compute() -> fclose(input_file_) #12");

												if (fclose(output_file_) == EOF)
													perror("compute() -> fclose(output_file_) #6");

												if (fclose(key_file_) == EOF)
													perror("compute() -> fclose(key_file_) #3");

												return EXIT_FAILURE;
											}
											else
												printf("\b\b\b\b%3d%%", get_percentage(input_file_size_, total_bytes_written_));
										}
									}
								}

								if (fclose(input_file_) == EOF)
									perror("compute() -> fclose(input_file_) #11");

								if (fclose(output_file_) == EOF)
									perror("compute() -> fclose(output_file_) #5");

								if (fclose(key_file_) == EOF)
									perror("compute() -> fclose(key_file_) #2");
							}
				}

	if (_global_run_flag_ == XORFD_CONTINUE)
		printf(" - operation completed.");
	else
		fprintf(stderr, "cleanup OK.\n\n");

	printf("\n* Total bytes read:\t%llu\n"
		"* Total bytes written:\t%llu\n\n",
		(unsigned long long)total_bytes_read_,
		(unsigned long long)total_bytes_written_);

	return EXIT_SUCCESS;
} // ### int compute(const char *argument1, const char *argument2, const char *argument3)

// ### MAIN FUNCTION ###
int main(int argc, char **argv)
// ### MAIN FUNCTION ###
{
	if (argc > 4)
	{
		print_usage(argv[0], "Too many arguments.");
		return EXIT_FAILURE;
	}
	else
		if (argc < 2)
		{
			if (argc < 1)
			// NOTE: this shouldn't normally be possible except with a
			//  specially crafted exec() call from another program
			{
				print_usage(GENERIC_FILENAME, "Nice try.");
				return EXIT_FAILURE;
			}
			else
			{
				print_usage(argv[0], "The program needs at least one argument.");
				return EXIT_FAILURE;
			}
		}
		else
		{
			signals_setup();

			switch (argc)
			{
				case 2: return compute(argv[1], NULL, NULL);		// case 1: input file overwritten, key from standard input
				case 3: return compute(argv[1], argv[2], NULL);		// case 2: input file, output file, key from standard input
				case 4: return compute(argv[1], argv[2], argv[3]);	// case 3: input file, output file, key file
				default:
					print_usage(argv[0], "Unknown. Email <rfahy@ymail.com> and include detail, please.");
					return EXIT_FAILURE;
			}
		}
} // ### MAIN FUNCTION ###
