/*
 * Copyright (C) 2012 Cyril Hrubis chrubis@suse.cz
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it would be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * Further, this software is distributed without any warranty that it is
 * free of the rightful claim of any third person regarding infringement
 * or the like.  Any license provided herein, whether implied or
 * otherwise, applies only to this software file.  Patent licenses, if
 * any, provided herein do not apply to combinations of this program with
 * other software, or any other product whatsoever.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "config.h"
#include <stdarg.h>
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <utime.h>

#include "test.h"
#include "safe_file_ops_fn.h"

/*
 * Count number of expected assigned conversions. Any conversion starts with '%'.
 * The '%%' matches % and no assignment is done. The %*x matches as x would do but
 * the assignment is suppressed.
 *
 * NOTE: This is not 100% correct for complex scanf strings, but will do for
 *       all of our intended usage.
 */
static int count_scanf_conversions(const char *fmt)
{
	unsigned int cnt = 0;
	int flag = 0;

	while (*fmt) {
		switch (*fmt) {
		case '%':
			if (flag) {
				cnt--;
				flag = 0;
			} else {
				flag = 1;
				cnt++;
			}
			break;
		case '*':
			if (flag) {
				cnt--;
				flag = 0;
			}
			break;
		default:
			flag = 0;
		}

		fmt++;
	}

	return cnt;
}

int file_scanf(const char *file, const int lineno,
		     const char *path, const char *fmt, ...)
{
	va_list va;
	FILE *f;
	int exp_convs, ret;

	f = fopen(path, "r");

	if (f == NULL) {
		tst_resm(TWARN,
			"Failed to open FILE '%s' at %s:%d",
			 path, file, lineno);
		return 1;
	}

	exp_convs = count_scanf_conversions(fmt);

	va_start(va, fmt);
	ret = vfscanf(f, fmt, va);
	va_end(va);

	if (ret == EOF) {
		tst_resm(TWARN,
			 "The FILE '%s' ended prematurely at %s:%d",
			 path, file, lineno);
		goto err;
	}

	if (ret != exp_convs) {
		tst_resm(TWARN,
			"Expected %i conversions got %i FILE '%s' at %s:%d",
			 exp_convs, ret, path, file, lineno);
		goto err;
	}

	if (fclose(f)) {
		tst_resm(TWARN,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
		return 1;
	}

	return 0;

err:
	if (fclose(f)) {
		tst_resm(TWARN,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
	}
	return 1;
}

void safe_file_scanf(const char *file, const int lineno,
		     void (*cleanup_fn) (void),
		     const char *path, const char *fmt, ...)
{
	va_list va;
	FILE *f;
	int exp_convs, ret;

	f = fopen(path, "r");

	if (f == NULL) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			 "Failed to open FILE '%s' for reading at %s:%d",
			 path, file, lineno);
		return;
	}

	exp_convs = count_scanf_conversions(fmt);

	va_start(va, fmt);
	ret = vfscanf(f, fmt, va);
	va_end(va);

	if (ret == EOF) {
		tst_brkm(TBROK, cleanup_fn,
			 "The FILE '%s' ended prematurely at %s:%d",
			 path, file, lineno);
		return;
	}

	if (ret != exp_convs) {
		tst_brkm(TBROK, cleanup_fn,
			 "Expected %i conversions got %i FILE '%s' at %s:%d",
			 exp_convs, ret, path, file, lineno);
		return;
	}

	if (fclose(f)) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
		return;
	}
}


/*
 * Try to parse each line from file specified by 'path' according
 * to scanf format 'fmt'. If all fields could be parsed, stop and
 * return 0, otherwise continue or return 1 if EOF is reached.
 */
int file_lines_scanf(const char *file, const int lineno,
		     void (*cleanup_fn)(void), int strict,
		     const char *path, const char *fmt, ...)
{
	FILE *fp;
	int ret = 0;
	int arg_count = 0;
	char line[BUFSIZ];
	va_list ap;

	if (!fmt) {
		tst_brkm(TBROK, cleanup_fn, "pattern is NULL, %s:%d",
			file, lineno);
		return 1;
	}

	fp = fopen(path, "r");
	if (fp == NULL) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			"Failed to open FILE '%s' for reading at %s:%d",
			path, file, lineno);
		return 1;
	}

	arg_count = count_scanf_conversions(fmt);

	while (fgets(line, BUFSIZ, fp) != NULL) {
		va_start(ap, fmt);
		ret = vsscanf(line, fmt, ap);
		va_end(ap);

		if (ret == arg_count)
			break;
	}
	fclose(fp);

	if (strict && ret != arg_count) {
		tst_brkm(TBROK, cleanup_fn, "Expected %i conversions got %i"
			" at %s:%d", arg_count, ret, file, lineno);
		return 1;
	}

	return !(ret == arg_count);
}

int file_printf(const char *file, const int lineno,
		      const char *path, const char *fmt, ...)
{
	va_list va;
	FILE *f;

	f = fopen(path, "w");

	if (f == NULL) {
		tst_resm(TWARN,
			 "Failed to open FILE '%s' at %s:%d",
			 path, file, lineno);
		return 1;
	}

	va_start(va, fmt);

	if (vfprintf(f, fmt, va) < 0) {
		tst_resm(TWARN,
			"Failed to print to FILE '%s' at %s:%d",
			 path, file, lineno);
		goto err;
	}

	va_end(va);

	if (fclose(f)) {
		tst_resm(TWARN,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
		return 1;
	}

	return 0;

err:
	if (fclose(f)) {
		tst_resm(TWARN,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
	}
	return 1;
}

void safe_file_printf(const char *file, const int lineno,
		      void (*cleanup_fn) (void),
		      const char *path, const char *fmt, ...)
{
	va_list va;
	FILE *f;

	f = fopen(path, "w");

	if (f == NULL) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			 "Failed to open FILE '%s' for writing at %s:%d",
			 path, file, lineno);
		return;
	}

	va_start(va, fmt);

	if (vfprintf(f, fmt, va) < 0) {
		tst_brkm(TBROK, cleanup_fn,
			 "Failed to print to FILE '%s' at %s:%d",
			 path, file, lineno);
		return;
	}

	va_end(va);

	if (fclose(f)) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			 "Failed to close FILE '%s' at %s:%d",
			 path, file, lineno);
		return;
	}
}

//TODO: C implementation? better error condition reporting?
void safe_cp(const char *file, const int lineno,
	     void (*cleanup_fn) (void), const char *src, const char *dst)
{
	size_t len = strlen(src) + strlen(dst) + 16;
	char buf[len];
	int ret;

	snprintf(buf, sizeof(buf), "cp \"%s\" \"%s\"", src, dst);

	ret = system(buf);

	if (ret) {
		tst_brkm(TBROK, cleanup_fn,
			 "Failed to copy '%s' to '%s' at %s:%d",
			 src, dst, file, lineno);
	}
}

#ifndef HAVE_UTIMENSAT

static void set_time(struct timeval *res, const struct timespec *src,
			long cur_tv_sec, long cur_tv_usec)
{
	switch (src->tv_nsec) {
	case UTIME_NOW:
	break;
	case UTIME_OMIT:
		res->tv_sec = cur_tv_sec;
		res->tv_usec = cur_tv_usec;
	break;
	default:
		res->tv_sec = src->tv_sec;
		res->tv_usec = src->tv_nsec / 1000;
	}
}

#endif

void safe_touch(const char *file, const int lineno,
		void (*cleanup_fn)(void),
		const char *pathname,
		mode_t mode, const struct timespec times[2])
{
	int ret;
	mode_t defmode;

	defmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;

	ret = open(pathname, O_CREAT | O_WRONLY, defmode);
	if (ret == -1) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			"Failed to open file '%s' at %s:%d",
			pathname, file, lineno);
		return;
	}

	ret = close(ret);
	if (ret == -1) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			"Failed to close file '%s' at %s:%d",
			pathname, file, lineno);
		return;
	}

	if (mode != 0) {
		ret = chmod(pathname, mode);
		if (ret == -1) {
			tst_brkm(TBROK | TERRNO, cleanup_fn,
				"Failed to chmod file '%s' at %s:%d",
				pathname, file, lineno);
			return;
		}
	}


#ifdef HAVE_UTIMENSAT
	ret = utimensat(AT_FDCWD, pathname, times, 0);
#else
	if (times == NULL) {
		ret = utimes(pathname, NULL);
	} else {
		struct stat sb;
		struct timeval cotimes[2];

		ret = stat(pathname, &sb);
		if (ret == -1) {
			tst_brkm(TBROK | TERRNO, cleanup_fn,
				"Failed to stat file '%s' at %s:%d",
				pathname, file, lineno);
			return;
		}

		ret = gettimeofday(cotimes, NULL);
		if (ret == -1) {
			tst_brkm(TBROK | TERRNO, cleanup_fn,
				"Failed to gettimeofday() at %s:%d",
				file, lineno);
			return;
		}

		cotimes[1] = cotimes[0];

		set_time(cotimes, times,
			sb.st_atime, sb.st_atim.tv_nsec / 1000);
		set_time(cotimes + 1, times + 1,
			sb.st_mtime, sb.st_mtim.tv_nsec / 1000);

		ret = utimes(pathname, cotimes);
	}
#endif
	if (ret == -1) {
		tst_brkm(TBROK | TERRNO, cleanup_fn,
			"Failed to update the access/modification time on file"
			" '%s' at %s:%d", pathname, file, lineno);
	}
}
