/*****************************************************//**
 * @brief	Prmitive hexadecimal file viewer (pipe it to 'less' or 'more'
 *		to make it much less primitive... see Usage below).
 * @file	hexview.c
 * @version	0.1A
 * @date	21 Feb, 2012
 * @author 	migf1 <mig_f1@hotmail.com>
 * @par Language:
 *		C (ANSI C89)
 * @par Usage:
 *		hexview [-raw] [filename]
 *		\n
 *		Use -raw as the 1st command-line argument to force raw output
 *		(useful for piping, e.g: hexview -raw file | less)
 *
 * @remark	Feel free to experiment with the values of the pre-processor
 * 		constants: FMT_NCOLS, FMT_PGBREAK and FMT_POS. They affect the
 *		appearance of the output.
 *********************************************************
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAXINPUT	(1024+1)

#define FMT_FPOS	8		/* width in # of chars for the file-pos field */
#define FMT_NCOLS	16		/* # of columns in Bytes area(no more than 16)*/
#define FMT_PGBREAK	20		/* # of lines before page-break appears       */

#define pressENTER()							\
do{									\
	char mYcHAr;							\
	printf( "press ENTER..." );					\
	while ( (mYcHAr=getchar()) != '\n' && mYcHAr != EOF )		\
		;							\
}while(0)

typedef unsigned char Byte;

/*********************************************************//**
 * Read a c-string from stdin.
 *************************************************************
 */
char *s_read( char *s, size_t maxlen )
{
	size_t len = 0U;

	if ( !s || !fgets( s, maxlen, stdin) )
		return NULL;

	if ( maxlen > MAXINPUT)
		maxlen = MAXINPUT;

	if ( s[ (len=strlen(s))-1 ] == '\n')
		s[ len-1 ] = '\0';

	return s;
}

/*********************************************************//**
 * Return user selection on page-breaks
 *************************************************************
 */
char on_pagebreak(void)
{
	char input[ MAXINPUT ] = {'\0'};

	printf("--- Q to quit, ENTER to continue --- ");
	fflush( stdout );

	if ( !fgets( input, MAXINPUT, stdin ) )
		return '\0';

	return tolower( *input );
}

/*********************************************************//**
 * Display text labels and other info on the currently displayed page.
 *************************************************************
 */
void show_header( const char *fname, unsigned long int pg )
{
	unsigned short int i;
	const unsigned short totalcols = (FMT_FPOS + 1) + (3*FMT_NCOLS) + FMT_NCOLS;

	if ( !fname || !*fname )
		return;

	/* text labels for Bytes & Chars areas */
	printf( "%*s | page %lu\n", totalcols/2,fname, pg);
	printf( "%-*c %-*s%-*s\n", FMT_FPOS,' ', (3*FMT_NCOLS),"Bytes area", FMT_NCOLS,"Chars area" );

	/* text label for file-position */
	printf("%-*s ", FMT_FPOS,"Pos");

	/* hex indicies for Bytes */
	for (i=0; i < FMT_NCOLS; i++)
		printf( "%-2hX ", i);

	/* hex indicies for Characters */
	for (i=0; i < FMT_NCOLS; i++)
		printf( "%hX", i);
	putchar('\n');

	/* separating line */
	for (i=0; i < totalcols; i++)
		putchar('-');
	putchar('\n');


	return;
}

/*********************************************************//**
 * List the contents of a file in hex/char tabular format.
 *************************************************************
 */
int file_hexlist( const char *fname, const unsigned short int israw )
{
	Byte buffer[ FMT_NCOLS ] = {0};		/* for reading file chunks into memory*/
	unsigned long int i, row, pg;		/* byte, row and page counters        */
	FILE *fp = NULL;			/* file pointer                       */

	if ( !fname || !*fname || NULL == (fp=fopen(fname, "rb")) )
		return 0;	/* FALSE */

	pg = 0;
	for (i=0,row=0; !feof(fp); row++,i+=FMT_NCOLS )
	{
		unsigned int 	j;
		size_t 		n;

		/* on page break */
		if ( !israw && row % FMT_PGBREAK == 0 ) {
			if ( row != 0 )
				if ( 'q' == on_pagebreak() )
					break;
			show_header(fname, pg+1);
			pg++;
		}

		memset( buffer, 0, FMT_NCOLS * sizeof(Byte) );
		n = fread(buffer, sizeof(Byte), FMT_NCOLS, fp);

		printf( "%08lX ", i);

		for (j=0; j < n; j++)
			printf("%02hX ", buffer[j] );
		for (; j < FMT_NCOLS; j++)
			printf("   ");

		for (j=0; j < n; j++)
			putchar( isprint(buffer[j]) ? buffer[j] : '.' );
		for (; j < FMT_NCOLS; j++)
			putchar(' ');
		putchar('\n');
	}
	putchar('\n');

	fclose(fp);
	return 1;		/* TRUE */
}

/*********************************************************//**
 *
 *************************************************************
 */
int main( int argc, char *argv[] )
{
	char fname[ MAXINPUT ] = {'\0'};	/* name of the file to be viewed    */
	unsigned short int israw = 0;		/* FALSE */

	/* parse the command line */
	if ( 1 == argc )			/* no arguments in the command line */
	{
		printf("Enter name of the file: ");
		s_read( fname, MAXINPUT );
	}
	else					/* command line contains arguments */
	{
		if ( !strcmp(argv[1], "-raw") )
			israw = 1;		/* TRUE */
		else
			strncpy( fname, argv[1], MAXINPUT-1 );

		if ( israw && argc > 2 )
			strncpy( fname, argv[2], MAXINPUT-1 );
		else if ( israw && argc == 2  ) {
			printf("Enter name of the file: ");
			s_read( fname, MAXINPUT );
		}
	}

	/* list the file contents */
	if ( !file_hexlist(fname, israw) ) {
		perror(NULL);
		goto exit_failure;
	}

	if ( !israw )
		pressENTER();
	exit( EXIT_SUCCESS );

exit_failure:
	if ( !israw )
		pressENTER();
	exit( EXIT_FAILURE );
}
