/* vim: set ts=8 sts=4 sw=4 tw=80 noet: */
/*======================================================================
Copyright (C) 2004,2005,2009 Walter Doekes <walter+tthsum@wjd.nu>
This file is part of tthsum.

tthsum 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 3 of the License, or
(at your option) any later version.

tthsum 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 tthsum.  If not, see <http://www.gnu.org/licenses/>.
======================================================================*/
#include "tthsum.h"

#include "base32.h"
#include "escape.h"
#include "read.h"
#include "thex.h"
#include "types.h"
#include "utf8.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef USE_TEXTS
#   include "texts.h"
#else /* !USE_TEXTS */
#   define get_error() "Fail"
#endif /* !USE_TEXTS */

#define MY_PATH_MAX 32768


static void tthsum_progress(uint64_t current_pos, uint64_t end_pos);
static struct rofile* tthsum_rofopen(const char* filename,
	const struct tthsum_options* options);

static int tthsum_generate_root(const char* filename,
	const struct tthsum_options* opt) {
    struct rofile* stream;
    uint64_t hash[3];
    char hash_base32[40];
    /* unicode will be at most equal sized to the multibyte charset */
    wchar_t filename_uni[MY_PATH_MAX + 1];
    /* utf8/C-escaping will be at most 4 times the mbcs equivalent */
    char filename_buf[MY_PATH_MAX * 4 + 1];

    if (filename != NULL && strlen(filename) > MY_PATH_MAX) {
#ifdef USE_TEXTS
	set_error("tthsum_generate_root", TTHSUM_FILENAME_TOO_LARGE);
#endif /* USE_TEXTS */
	return -1;
    }

    if ((stream = tthsum_rofopen(filename, opt)) == NULL
	    || thex_tiger_root(stream, hash,
		    opt->progress_every_mib ? tthsum_progress : NULL) == -1) {
	if (stream != NULL)
	    rofclose(stream);
	return -1;
    }
    rofclose(stream);

    if (uint64tobase32(hash_base32, hash, 3) == -1)
	return -1;
    if (filename != NULL) {
	/* the order of calling to_utf8 or to_ctrlesc doesn't matter,
	   as they operate on chars 0-127 and 128-255 respectively. */
#ifdef _WIN32
	/* change all \\ to / on Windows */
	char filename_tmp[MY_PATH_MAX + 1];
	char* p;
	for (p = filename_tmp; *filename != '\0'; ++filename)
	    if (*filename == '\\')
		*p++ = '/';
	    else
		*p++ = *filename;
	*p = '\0';
	strtoctrlesc(filename_buf, filename_tmp);
#else /* !_WIN32 */
	strtoctrlesc(filename_buf, filename);
#endif /* !_WIN32 */
	if (mbstowcs(filename_uni, filename_buf, MY_PATH_MAX + 1)
		== (size_t)-1) {
	    if (!opt->has_locale)
		fprintf(stderr, "tthsum: warning: Locale settings are C/POSIX. "
			"See the tthsum manpage for help.\n");
#ifdef USE_TEXTS
	    set_error("mbstowcs", -1);
#endif /* USE_TEXTS */
	    return -1;
	}
	if (wcstoutf8(filename_buf, filename_uni, MY_PATH_MAX * 4 + 1)
		== (size_t)-1)
	    return -1;
    } else {
	strcpy(filename_buf, "-");
    }

    /* print our root hash with utf8/escaped filename */
    printf("%s  %s\n", hash_base32, filename_buf);
    return 0;
}

int tthsum_generate_roots(const char* filenames[], int files,
	const struct tthsum_options* opt) {
    int i;
    for (i = 0; i < files; ++i) {
	if (tthsum_generate_root(filenames[i], opt) == -1)
#ifdef USE_TEXTS
	    fprintf(stderr, "tthsum: `%s': %s\n",
		    filenames[i] ? filenames[i] : "-", get_error());
#else /* !USE_TEXTS */
	    fprintf(stderr, "tthsum: `%s': Fail\n",
		    filenames[i] ? filenames[i] : "-");
#endif /* !USE_TEXTS */
    }
    return 0;
}

static int tthsum_check_digest_line(char* line,
	unsigned line_len, const struct tthsum_options* opt) {
    /* 39char base32, 2 spaces, MY_PATH_MAX utf8/C-esc file, 1 terminating \0 */
    wchar_t filename_uni[MY_PATH_MAX * 4 + 1];
    char filename_mbs[MY_PATH_MAX * 4 + 1];
    char filename[MY_PATH_MAX * 4 + 1];
    struct rofile* stream;
    uint64_t hash_line[3] = {_ULL(0), _ULL(0), _ULL(0)};
    uint64_t hash_file[3];
    char* linep = line;
    char tmp;

    /* must be at least 39 base32 + 2 spaces + 1 filename long */
    if (line_len < 42 || line_len > (MY_PATH_MAX * 4 + 39 + 2)) {
#ifdef USE_TEXTS
	set_error("tthsum_check_digest_line", TTHSUM_LINE_CORRUPT);
#endif /* USE_TEXTS */
	return -1;
    }
    /* read base32 hash */
    if (base32touint64(hash_line, linep, 3) == -1)
	return -1;
    /* check the two spaces */
    linep += 39;
    if (*linep != ' ' || *++linep != ' ') {
#ifdef USE_TEXTS
	set_error("tthsum_check_digest_line", TTHSUM_LINE_CORRUPT);
#endif /* USE_TEXTS */
	return -1;
    }
    ++linep;
    /* fetch filename and convert to local codepage.
     * unfortunately our windows utf8 routine doesn't return success unless
     * it has found a terminating null, sigh */
    tmp = *(line + line_len);
    *(line + line_len) = '\0';
    if (utf8towcs(filename_uni, linep, MY_PATH_MAX * 4 + 1)
	    == (size_t)-1) {
	*(line + line_len) = tmp;
	return -1;
    }
    *(line + line_len) = tmp; /* yes, uncorrupting the crap */
    filename_uni[MY_PATH_MAX * 4] = '\0'; /* make sure we're terminated */
    /* proceed */
    if (wcstombs(filename_mbs, filename_uni, MY_PATH_MAX * 4 + 1)
	    == (size_t)-1) {
#ifdef USE_TEXTS
	set_error("wcstombs", -1);
#endif /* USE_TEXTS */
	return -1;
    }
    filename_mbs[MY_PATH_MAX * 4] = '\0'; /* make sure we're terminated */
    if (ctrlesctostr(filename, filename_mbs) == -1)
	return -1;

    /* w00t, got filename. go check TTH on it */
    if ((stream = tthsum_rofopen(filename, opt)) == NULL
	    || thex_tiger_root(stream, hash_file,
		    opt->progress_every_mib ? tthsum_progress : NULL) == -1) {
	if (opt->verbose)
	    fprintf(stderr, "%-14s FAILED (%s)\n", filename, get_error());
	else
	    fprintf(stderr, "tthsum: `%s': %s\n", filename, get_error());
	if (stream != NULL)
	    rofclose(stream);
	return 0;
    }
    rofclose(stream);

    /* compare tth's */
    if (memcmp(hash_line, hash_file, 3 * 8) == 0) {
	if (opt->verbose)
	    printf("%-14s OK\n", filename);
	return 1;
    }

    /* not equal, print an error */
    if (!opt->verbose) {
#ifdef USE_TEXTS
	fprintf(stderr, "tthsum: %s `%s'\n", get_text(TTHSUM_MISMATCHED_TTH),
		filename);
#else /* !USE_TEXTS */
	fprintf(stderr, "tthsum: TTH check failed for `%s'\n", filename);
#endif /* !USE_TEXTS */
    } else {
	printf("%-14s FAILED\n", filename);
    }
    return 0;
}
    
int tthsum_check_digest(const char* filename,
	const struct tthsum_options* opt) {
    unsigned filesize;
    char* file_buf = rof_readall(
	filename == NULL ? rofopen_sysfd_stdin() : rofopen_sysfile(filename),
	&filesize
    );
    char* file_buf_end;
    char* s;
    char* e;
    unsigned line_no = 1;
    unsigned lines_correct = 0;
    unsigned lines_incorrect = 0;
    unsigned files_correct = 0;

    if (filename == NULL)
	filename = "-";
    if (file_buf == NULL) {
#ifdef USE_TEXTS
	fprintf(stderr, "tthsum: `%s': %s\n", filename, get_error());
#else /* !USE_TEXTS */
	fprintf(stderr, "tthsum: `%s': Fail\n", filename);
#endif /* !USE_TEXTS */
	return 1;
    }
    /* Operate on a line by line basis.. we would like to get the most out
     * of corrupt or un-decodable files (esp. if the decoding fails for some
     * files only) */
    file_buf_end = file_buf + filesize;
    s = e = file_buf;
    while (1) {
	int ret;
	/* find \r or \n, or really, any ctrl-character will do, as they're all
	   escaped */
	while (e != file_buf_end && (unsigned char)*e >= 0x20)
	    ++e;
	/* test the line */
	ret = tthsum_check_digest_line(s, e - s, opt);
	if (ret == 1) {
	    ++lines_correct;
	    ++files_correct;
	} else if (ret == 0) {
	    ++lines_correct;
	} else {
	    if (opt->warn)
#ifdef USE_TEXTS
		fprintf(stderr, "tthsum: `%s': %u: %s\n", filename, line_no,
			get_error());
#else /* !USE_TEXTS */
		fprintf(stderr, "tthsum: `%s': %u: Fail\n", filename, line_no);
#endif /* !USE_TEXTS */
	    ++lines_incorrect;
	}
	/* find the only true line separator, the \n */
	s = e;
	while (s != file_buf_end && *s != '\n')
	    ++s;
	if (s == file_buf_end || s + 1 == file_buf_end) /* expect \n at eof */
	    break;
	/* start on the next line */
	e = ++s;
	++line_no;
    }
    /* free our readfile data */
    free(file_buf);

    if (files_correct == 0 && lines_correct == 0) {
	fprintf(stderr, "tthsum: `%s': No files checked\n", filename);
	return -1;
    } else if (lines_correct != files_correct) {
	if (opt->verbose)
	    fprintf(stderr, "tthsum: %u of %u file(s) failed TTH check\n",
		    lines_correct - files_correct, files_correct);
	return -1;
    }
    return 0;
}

static void tthsum_progress(uint64_t current_pos, uint64_t end_pos) {
    if (end_pos != (uint64_t)-1)
	fprintf(stderr, "(%0.1f%%)\r",
		100.0 * (float)current_pos / (float)end_pos);
    else
	fprintf(stderr, "(%" PRIu64 ")\r", current_pos);
}

static struct rofile* tthsum_rofopen(const char* filename,
	const struct tthsum_options* options) {
    if (filename == NULL)
	return rofopen_sysfd_stdin();
    else if (!options->use_mmap)
	return rofopen_sysfile(filename);
    return rofopen_mmap(filename);
}
