/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file
 */

#include "lib.h"
#include "compat.h"
#include "unichar.h"
#include "array.h"
#include "istream.h"
#include "eacces-error.h"

#include "sieve-common.h"
#include "sieve-limits.h"
#include "sieve-error.h"

#include "sieve-script-private.h"

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

/*
 * Configuration
 */
 
#define SIEVE_READ_BLOCK_SIZE (1024*8)

/*
 * Script name
 */

bool sieve_script_name_is_valid(const char *scriptname)
{
	ARRAY_TYPE(unichars) uni_name;
	unsigned int count, i;
	const unichar_t *name_chars;
	size_t namelen = strlen(scriptname);

	/* Check maximum length */
	if ( namelen > SIEVE_MAX_SCRIPT_NAME_LEN )
		return FALSE;

	/* Intialize array for unicode characters */
	t_array_init(&uni_name, namelen * 4);

	/* Convert UTF-8 to UCS4/UTF-32 */
	if ( uni_utf8_to_ucs4(scriptname, &uni_name) < 0 )
		return FALSE;

	/* Scan name for invalid characters */
	name_chars = array_get(&uni_name, &count);
	for ( i = 0; i < count; i++ ) {

		/* 0000-001F; [CONTROL CHARACTERS] */
		if ( name_chars[i] <= 0x001f )
			return FALSE;
		
		/* 002F; SLASH */
		if ( name_chars[i] == 0x002f )
			return FALSE;

		/* 007F; DELETE */
		if ( name_chars[i] == 0x007f )
			return FALSE;

		/* 0080-009F; [CONTROL CHARACTERS] */
		if ( name_chars[i] >= 0x0080 && name_chars[i] <= 0x009f )
			return FALSE;

		/* 2028; LINE SEPARATOR */
		/* 2029; PARAGRAPH SEPARATOR */
		if ( name_chars[i] == 0x2028 || name_chars[i] == 0x2029 )
			return FALSE;
	}

	return TRUE;
}

/*
 * Filename to name/name to filename
 */

static inline const char *_sieve_scriptfile_get_basename(const char *filename)
{
	const char *ext;

	/* Extract the script name */
	ext = strrchr(filename, '.');
	if ( ext == NULL || ext == filename || strncmp(ext,".sieve",6) != 0 )
		return filename;
	
	return t_strdup_until(filename, ext);	
}

bool sieve_script_file_has_extension(const char *filename)
{
	const char *ext;

 	/* See if it ends in .sieve already */
	ext = strrchr(filename, '.');
	if ( ext == NULL || ext == filename || strncmp(ext,".sieve",6) != 0 )
		return FALSE;

	return TRUE;
}

static inline const char *_sieve_scriptfile_from_name(const char *name)
{
	if ( !sieve_script_file_has_extension(name) )
		return t_strconcat(name, ".sieve", NULL);

	return name;
}


/* 
 * Script object 
 */
 
struct sieve_script *sieve_script_init
(struct sieve_script *script, struct sieve_instance *svinst, 
	const char *path, const char *name, struct sieve_error_handler *ehandler, 
	bool *exists_r)
{
	int ret;
	pool_t pool;
	struct stat st;
	struct stat lnk_st;
	const char *filename, *dirpath, *basename, *binpath;

	if ( exists_r != NULL )
		*exists_r = TRUE;

	T_BEGIN {

		/* Extract filename from path */

		filename = strrchr(path, '/');
		if ( filename == NULL ) {
			dirpath = "";
			filename = path;
		} else {
			dirpath = t_strdup_until(path, filename);
			filename++;
		}

		basename = _sieve_scriptfile_get_basename(filename);

		if ( *dirpath == '\0' )
			binpath = t_strconcat(basename, ".svbin", NULL);
		else
			binpath = t_strconcat(dirpath, "/", basename, ".svbin", NULL);
				
		if ( name == NULL ) {
			name = basename; 
		} else if ( *name == '\0' ) {
			name = NULL;
		} else {
			basename = name;
		}
			
		/* First obtain stat data from the system */
		
		if ( (ret=lstat(path, &st)) < 0 ) {
			if ( errno == ENOENT ) {
				if ( exists_r == NULL ) 
					sieve_error(ehandler, basename, "sieve script does not exist");
				else
					*exists_r = FALSE;
			} else
				sieve_critical(ehandler, basename, 
					"failed to stat sieve script: lstat(%s) failed: %m", path);

			script = NULL;
			ret = 1;

		} else {
			/* Record stat information from the symlink */
			lnk_st = st;

			/* Only create/init the object if it stat()s without problems */
			if (S_ISLNK(st.st_mode)) {
				if ( (ret=stat(path, &st)) < 0 ) { 
					if ( errno == ENOENT ) {
						if ( exists_r == NULL )
							sieve_error(ehandler, basename, "sieve script does not exist");
						else
							*exists_r = FALSE;
					} else
						sieve_critical(ehandler, basename, 
							"failed to stat sieve script: stat(%s) failed: %m", path);

					script = NULL;	
					ret = 1;
				}
			}

			if ( ret == 0 && !S_ISREG(st.st_mode) ) {
				sieve_critical(ehandler, basename, 
					"sieve script file '%s' is not a regular file.", path);
				script = NULL;
				ret = 1;
			} 
		}

		if ( ret <= 0 ) {
			if ( script == NULL ) {
				pool = pool_alloconly_create("sieve_script", 1024);
				script = p_new(pool, struct sieve_script, 1);
				script->pool = pool;
			} else 
				pool = script->pool;
		
			script->refcount = 1;
			script->svinst = svinst;

			script->ehandler = ehandler;
			sieve_error_handler_ref(ehandler);
		
			script->st = st;
			script->lnk_st = lnk_st;
			script->path = p_strdup(pool, path);
			script->filename = p_strdup(pool, filename);
			script->dirpath = p_strdup(pool, dirpath);
			script->binpath = p_strdup(pool, binpath);
			script->basename = p_strdup(pool, basename);

			if ( name != NULL )
				script->name = p_strdup(pool, name);
			else
				script->name = NULL;
		}
	} T_END;	

	return script;
}

struct sieve_script *sieve_script_create
(struct sieve_instance *svinst, const char *path, const char *name, 
	struct sieve_error_handler *ehandler, bool *exists_r)
{
	return sieve_script_init(NULL, svinst, path, name, ehandler, exists_r);
}

struct sieve_script *sieve_script_create_in_directory
(struct sieve_instance *svinst, const char *dirpath, const char *name,
	struct sieve_error_handler *ehandler, bool *exists_r)
{
	const char *path;

	if ( dirpath[strlen(dirpath)-1] == '/' )
		path = t_strconcat(dirpath, 
			_sieve_scriptfile_from_name(name), NULL);
	else
		path = t_strconcat(dirpath, "/",
			_sieve_scriptfile_from_name(name), NULL);

	return sieve_script_init(NULL, svinst, path, name, ehandler, exists_r);
}

void sieve_script_ref(struct sieve_script *script)
{
	script->refcount++;
}

void sieve_script_unref(struct sieve_script **script)
{
	i_assert((*script)->refcount > 0);

	if (--(*script)->refcount != 0)
		return;

	if ( (*script)->stream != NULL )
		i_stream_destroy(&(*script)->stream);

	sieve_error_handler_unref(&(*script)->ehandler);

	pool_unref(&(*script)->pool);

	*script = NULL;
}

/* 
 * Accessors 
 */

const char *sieve_script_name(const struct sieve_script *script)
{
	return script->name;
}

const char *sieve_script_filename(const struct sieve_script *script)
{
	return script->filename;
}

const char *sieve_script_path(const struct sieve_script *script)
{
	return script->path;
}

const char *sieve_script_dirpath(const struct sieve_script *script)
{
	return script->dirpath;
}

const char *sieve_script_binpath(const struct sieve_script *script)
{
	return script->binpath;
}

mode_t sieve_script_permissions(const struct sieve_script *script)
{
	return script->st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO);
}

struct sieve_instance *sieve_script_svinst(const struct sieve_script *script)
{
	return script->svinst;
}

/* 
 * Stream manageement 
 */

struct istream *sieve_script_open
(struct sieve_script *script, bool *deleted_r)
{
	int fd;
	struct stat st;
	struct istream *result;

	if ( deleted_r != NULL )
		*deleted_r = FALSE;

	if ( (fd=open(script->path, O_RDONLY)) < 0 ) {
		if ( errno == ENOENT ) {
			if ( deleted_r == NULL ) 
				/* Not supposed to occur, create() does stat already */
				sieve_error(script->ehandler, script->basename, 
					"sieve script does not exist");
			else 
				*deleted_r = TRUE;
		} else if ( errno == EACCES ) {
			sieve_critical(script->ehandler, script->path,
				"failed to open sieve script: %s",
				eacces_error_get("open", script->path));
		} else {
			sieve_critical(script->ehandler, script->path, 
				"failed to open sieve script: open(%s) failed: %m", script->path);
		}
		return NULL;
	}	
	
	if ( fstat(fd, &st) != 0 ) {
		sieve_critical(script->ehandler, script->path, 
			"failed to open sieve script: fstat(fd=%s) failed: %m", script->path);
		result = NULL;
	} else {
		/* Re-check the file type just to be sure */
		if ( !S_ISREG(st.st_mode) ) {
			sieve_critical(script->ehandler, script->path,
				"sieve script file '%s' is not a regular file", script->path);
			result = NULL;
		} else if ( script->svinst->max_script_size > 0 && 
			(uoff_t)st.st_size > script->svinst->max_script_size ) {
			sieve_error(script->ehandler, script->basename,
				"sieve script is too large (max %"PRIuSIZE_T" bytes)",
				script->svinst->max_script_size);
			result = NULL;
		} else {
			result = script->stream = 
				i_stream_create_fd(fd, SIEVE_READ_BLOCK_SIZE, TRUE);
			script->st = script->lnk_st = st;
		}
	}

	if ( result == NULL ) {
		/* Something went wrong, close the fd */
		if ( close(fd) != 0 ) {
			sieve_sys_error(
				"failed to close sieve script: close(fd=%s) failed: %m", 
				script->path);
		}
	}
	
	return result;
}

void sieve_script_close(struct sieve_script *script)
{
	i_stream_destroy(&script->stream);
}

uoff_t sieve_script_get_size(const struct sieve_script *script)
{
	return script->st.st_size;
}

/* 
 * Comparison 
 */

int sieve_script_cmp
(const struct sieve_script *script1, const struct sieve_script *script2)
{
	if ( script1 == NULL || script2 == NULL ) 
		return -1;	

	return ( script1->st.st_ino == script2->st.st_ino ) ? 0 : -1;
}

unsigned int sieve_script_hash(const struct sieve_script *script)
{	
	return (unsigned int) script->st.st_ino;
}

bool sieve_script_newer
(const struct sieve_script *script, time_t time)
{
	return ( script->st.st_mtime > time || script->lnk_st.st_mtime > time );
}