/*
 * trust-file.c - Functions for working with trust files 
 * Copyright (c) 2020 Red Hat Inc.
 * All Rights Reserved.
 *
 * This software may be freely redistributed and/or modified under the
 * terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2, 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; see the file COPYING. If not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor
 * Boston, MA 02110-1335, USA.
 *
 * Authors:
 *   Zoltan Fridrich <zfridric@redhat.com>
 */

#include "config.h"

#include <ctype.h>
#include <fcntl.h>
#include <ftw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "fapolicyd-backend.h"
#include "file.h"
#include "llist.h"
#include "message.h"
#include "trust-file.h"
#include "escape.h"
#include "paths.h"

#define BUFFER_SIZE 4096+1+1+1+10+1+64+1
#define FILE_READ_FORMAT  "%4096s %lu %64s" // path size SHA256
#define FILE_WRITE_FORMAT "%s %lu %s\n"     // path size SHA256
#define FTW_NOPENFD 1024
#define FTW_FLAGS (FTW_ACTIONRETVAL | FTW_PHYS)

#define HEADER0 "# AUTOGENERATED FILE VERSION 2\n"
#define HEADER1 "# This file contains a list of trusted files\n"
#define HEADER2 "#\n"
#define HEADER3 "#  FULL PATH        SIZE                             SHA256\n"
#define HEADER4 "# /home/user/my-ls 157984 61a9960bf7d255a85811f4afcac51067b8f2e4c75e21cf4f2af95319d4ed1b87\n"


list_t _list;
char *_path;
int _count;



/**
 * Take a path and create a string that is ready to be written to the disk.
 *
 * @param path Path to create a string from
 * @return Data string ready to be written to the disk or NULL on error
 */
static char *make_data_string(const char *path)
{
	int fd = open(path, O_RDONLY);
	if (fd < 0) {
		msg(LOG_ERR, "Cannot open %s", path);
		return NULL;
	}

	// Get the size
	struct stat sb;
	if (fstat(fd, &sb)) {
		msg(LOG_ERR, "Cannot stat %s", path);
		close(fd);
		return NULL;
	}

	// Get the hash
	char *hash = get_hash_from_fd2(fd, sb.st_size);
	close(fd);
	if (!hash) {
		msg(LOG_ERR, "Cannot hash %s", path);
		return NULL;
	}

	char *line;
	/*
	 * formated data to be saved
	 * source size sha256
	 * path is stored as lmdb index
	 */
	int count = asprintf(&line, DATA_FORMAT, 0,
					  sb.st_size, hash);

	free(hash);

	if (count < 0) {
		msg(LOG_ERR, "Cannot format entry for %s", path);
		return NULL;
	}
	return line;
}

/**
 * Write a list into a file
 *
 * @param list List to write into a file
 * @param dest Destination file
 * @return 0 on success, 1 on error
 */
static int write_out_list(list_t *list, const char *dest)
{
	FILE *f = fopen(dest, "w");
	if (!f) {
		msg(LOG_ERR, "Cannot delete %s", dest);
		list_empty(list);
		return 1;
	}

	size_t hlen;
	hlen = strlen(HEADER0);
	fwrite(HEADER0, hlen, 1, f);

	hlen = strlen(HEADER1);
	fwrite(HEADER1, hlen, 1, f);

	hlen = strlen(HEADER2);
	fwrite(HEADER2, hlen, 1, f);

	hlen = strlen(HEADER3);
	fwrite(HEADER3, hlen, 1, f);

	hlen = strlen(HEADER4);
	fwrite(HEADER4, hlen, 1, f);

	for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) {
		char buf[BUFFER_SIZE + 1];
		const char *str = (char *)(lptr->data);
		const char *escaped = escape((char *)lptr->index, WHITESPACES);
		if (escaped == NULL) {
			msg(LOG_ERR, "Could not escape %s: writing to %s", (char *)lptr->index, dest);
			continue;
		}

		/*
		 * + 2 because we are omitting source number
		 * "0 12345 ..."
		 * 0 -> filedb source
		 */
		hlen = snprintf(buf, sizeof(buf), "%s %s\n", escaped, str + 2);
		fwrite(buf, hlen, 1, f);
		free((void *)escaped);
	}

	fclose(f);
	return 0;
}

int trust_file_append(const char *fpath, list_t *list)
{
	list_t content;
	list_init(&content);
	int rc = trust_file_load(fpath, &content);
	// if trust file does not exist, we ignore it as it will be created while writing
	if (rc == 2) {
		// exit on parse error, we dont want invalid entries to be autoremoved
		return 1;
	}

	for (list_item_t *lptr = list->first; lptr; lptr = lptr->next) {
		lptr->data = make_data_string(lptr->index);
	}

	list_merge(&content, list);
	write_out_list(&content, fpath);
	list_empty(&content);
	return 0;
}

/**
 * @brief Load trust file into list
 *
 * @param fpath Full path to trust file
 * @param list Trust file will be loaded into this list
 * @return 0 on success, 1 if file can't be open, 2 on parsing error
 */
int trust_file_load(const char *fpath, list_t *list)
{
	char buffer[BUFFER_SIZE];
	int escaped = 0;
	long line = 0;

	FILE *file = fopen(fpath, "r");
	if (!file)
		return 1;

	while (fgets(buffer, BUFFER_SIZE, file)) {
		char name[4097], sha[65], *index = NULL, *data = NULL;
		unsigned long sz;
		unsigned int tsource = SRC_FILE_DB;

		line++;

		if (iscntrl(buffer[0]) || buffer[0] == '#') {
			if (line == 1 && strncmp(buffer, HEADER0, strlen(HEADER0)) == 0)
				escaped = 1;
			continue;
		}

		if (sscanf(buffer, FILE_READ_FORMAT, name, &sz, sha) != 3) {
			msg(LOG_WARNING, "Can't parse %s", buffer);
			fclose(file);
			return 2;
		}

		if (asprintf(&data, DATA_FORMAT, tsource, sz, sha) == -1)
			data = NULL;

		index = escaped ? unescape(name) : strdup(name);
		if (index == NULL) {
			msg(LOG_ERR, "Could not unescape %s from %s", name, fpath);
			free(data);
			continue;
		}

		if (data == NULL) {
			free(index);
			continue;
		}

		if (list_contains(list, index)) {
			msg(LOG_WARNING, "%s contains a duplicate %s", fpath, index);
			free(index);
			free(data);
			continue;
		}

		if (list_append(list, index, data)) {
			free(index);
			free(data);
		}
	}

	fclose(file);
	return 0;
}

int trust_file_delete_path(const char *fpath, const char *path)
{
	list_t list;
	list_init(&list);
	int rc = trust_file_load(fpath, &list);
	switch (rc) {
	case 1:
		msg(LOG_ERR, "Cannot open %s", fpath);
		return 0;
	case 2:
		list_empty(&list);
		return -1;
	default:
		break;
	}

	int count = 0;
	size_t path_len = strlen(path);
	list_item_t *lptr = list.first, *prev = NULL, *tmp;

	while (lptr) {
		if (!strncmp(lptr->index, path, path_len)) {
			++count;
			tmp = lptr->next;

			if (prev)
				prev->next = lptr->next;
			else
				list.first = lptr->next;
			if (!lptr->next)
				list.last = prev;
			--list.count;
			list_destroy_item(&lptr);

			lptr = tmp;
			continue;
		}
		prev = lptr;
		lptr = lptr->next;
	}

	if (count)
		write_out_list(&list, fpath);

	list_empty(&list);
	return count;
}

int trust_file_update_path(const char *fpath, const char *path)
{
	list_t list;
	list_init(&list);
	int rc = trust_file_load(fpath, &list);
	switch (rc) {
	case 1:
		msg(LOG_ERR, "Cannot open %s", fpath);
		return 0;
	case 2:
		list_empty(&list);
		return -1;
	default:
		break;
	}

	int count = 0;
	size_t path_len = strlen(path);

	for (list_item_t *lptr = list.first; lptr; lptr = lptr->next) {
		if (!strncmp(lptr->index, path, path_len)) {
			free((char *)lptr->data);
			lptr->data = make_data_string(lptr->index);
			++count;
		}
	}

	if (count)
		write_out_list(&list, fpath);

	list_empty(&list);
	return count;
}

int trust_file_rm_duplicates(const char *fpath, list_t *list)
{
	list_t trust_file;
	list_init(&trust_file);
	int rc = trust_file_load(fpath, &trust_file);
	switch (rc) {
	case 1:
		msg(LOG_ERR, "Cannot open %s", fpath);
		return -1;
	case 2:
		list_empty(&trust_file);
		return -1;
	default:
		break;
	}

	for (list_item_t *lptr = trust_file.first; lptr; lptr = lptr->next) {
		list_remove(list, lptr->index);
		if (list->count == 0)
			break;
	}

	list_empty(&trust_file);
	return 0;
}



static int ftw_load(const char *fpath,
		const struct stat *sb __attribute__ ((unused)),
		int typeflag,
		struct FTW *ftwbuf __attribute__ ((unused)))
{
	if (typeflag == FTW_F)
		trust_file_load(fpath, &_list);
	return FTW_CONTINUE;
}

static int ftw_delete_path(const char *fpath,
		const struct stat *sb __attribute__ ((unused)),
		int typeflag,
		struct FTW *ftwbuf __attribute__ ((unused)))
{
	if (typeflag == FTW_F)
		_count += trust_file_delete_path(fpath, _path);
	return FTW_CONTINUE;
}

static int ftw_update_path(const char *fpath,
		const struct stat *sb __attribute__ ((unused)),
		int typeflag,
		struct FTW *ftwbuf __attribute__ ((unused)))
{
	if (typeflag == FTW_F)
		_count += trust_file_update_path(fpath, _path);
	return FTW_CONTINUE;
}

static int ftw_rm_duplicates(const char *fpath,
		const struct stat *sb __attribute__ ((unused)),
		int typeflag,
		struct FTW *ftwbuf __attribute__ ((unused)))
{
	if (_list.count == 0)
		return FTW_STOP;
	if (typeflag == FTW_F)
		trust_file_rm_duplicates(fpath, &_list);
	return FTW_CONTINUE;
}



void trust_file_load_all(list_t *list)
{
	list_empty(&_list);
	trust_file_load(TRUST_FILE_PATH, &_list);
	nftw(TRUST_DIR_PATH, &ftw_load, FTW_NOPENFD, FTW_FLAGS);
	list_merge(list, &_list);
}

int trust_file_delete_path_all(const char *path)
{
	_path = strdup(path);
	_count = trust_file_delete_path(TRUST_FILE_PATH, path);
	nftw(TRUST_DIR_PATH, &ftw_delete_path, FTW_NOPENFD, FTW_FLAGS);
	free(_path);
	return _count;
}

int trust_file_update_path_all(const char *path)
{
	_path = strdup(path);
	_count = trust_file_update_path(TRUST_FILE_PATH, path);
	nftw(TRUST_DIR_PATH, &ftw_update_path, FTW_NOPENFD, FTW_FLAGS);
	free(_path);
	return _count;
}

void trust_file_rm_duplicates_all(list_t *list)
{
	list_empty(&_list);
	list_merge(&_list, list);
	trust_file_rm_duplicates(TRUST_FILE_PATH, &_list);
	nftw(TRUST_DIR_PATH, &ftw_rm_duplicates, FTW_NOPENFD, FTW_FLAGS);
	list_merge(list, &_list);
}
