/* Database file handling routines.
 *
 * IRC Services is copyright (c) 1996-2002 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#ifndef CONVERT_DB
# include "modules.h"
#endif
#include "fileutil.h"
#include <fcntl.h>

/*************************************************************************/

#ifndef CONVERT_DB
static Module *module;
#endif

/*************************************************************************/
/*************************************************************************/

/* Return the version number of the file.  Return -1 if there is no version
 * number or the number doesn't make sense (i.e. less than 1).
 */

int32 get_file_version(dbFILE *f)
{
    FILE *fp = f->fp;
    int version = fgetc(fp)<<24 | fgetc(fp)<<16 | fgetc(fp)<<8 | fgetc(fp);
    if (ferror(fp)) {
#ifndef CONVERT_DB
	module_log_perror("Error reading version number on %s", f->filename);
#endif
	return -1;
    } else if (feof(fp)) {
#ifndef CONVERT_DB
	module_log("Error reading version number on %s: End of file detected",
		   f->filename);
#endif
	return -1;
    } else if (version < 1) {
#ifndef CONVERT_DB
	module_log("Invalid version number (%d) on %s", version, f->filename);
#endif
	return -1;
    }
    return version;
}

/*************************************************************************/

/* Write the version number to the file.  Return 0 on success, -1 on
 * failure.
 */

int write_file_version(dbFILE *f, int32 filever)
{
    FILE *fp = f->fp;
    if (
	fputc(filever>>24 & 0xFF, fp) < 0 ||
	fputc(filever>>16 & 0xFF, fp) < 0 ||
	fputc(filever>> 8 & 0xFF, fp) < 0 ||
	fputc(filever     & 0xFF, fp) < 0
    ) {
#ifndef CONVERT_DB
	module_log_perror("Error writing version number on %s", f->filename);
#endif
	return -1;
    }
    return 0;
}

/*************************************************************************/
/*************************************************************************/

static dbFILE *open_db_read(const char *filename)
{
    dbFILE *f;
    FILE *fp;

    f = smalloc(sizeof(*f));
    *f->tempname = 0;
    strscpy(f->filename, filename, sizeof(f->filename));
    f->mode = 'r';
    fp = fopen(f->filename, "rb");
    if (!fp) {
	int errno_save = errno;
#ifndef CONVERT_DB
	if (errno != ENOENT)
	    module_log_perror("Can't read database file %s", f->filename);
#endif
	free(f);
	errno = errno_save;
	return NULL;
    }
    f->fp = fp;
    return f;
}

/*************************************************************************/

static dbFILE *open_db_write(const char *filename, int32 filever)
{
    dbFILE *f;
    int fd;

    f = smalloc(sizeof(*f));
    *f->tempname = 0;
    strscpy(f->filename, filename, sizeof(f->filename));
    filename = f->filename;
    f->mode = 'w';

    snprintf(f->tempname, sizeof(f->tempname), "%s.new", filename);
    if (!*f->tempname || strcmp(f->tempname, filename) == 0) {
#ifndef CONVERT_DB
	module_log("Opening database file %s for write: Filename too long",
		   filename);
#endif
	free(f);
	errno = ENAMETOOLONG;
	return NULL;
    }
    remove(f->tempname);
    /* Use open() to avoid people sneaking a new file in under us */
    fd = open(f->tempname, O_WRONLY | O_CREAT | O_EXCL, 0666);
    if (fd >= 0)
	f->fp = fdopen(fd, "wb");
    if (!f->fp || write_file_version(f, filever) < 0) {
	int errno_save = errno;
#ifndef CONVERT_DB
	static int walloped = 0;
	if (!walloped) {
	    walloped++;
	    wallops(NULL, "Can't create temporary database file %s",
		    f->tempname);
	}
	errno = errno_save;
	module_log_perror("Can't create temporary database file %s",
			  f->tempname);
#endif
	if (f->fp)
	    fclose(f->fp);
	remove(f->tempname);
	errno = errno_save;
	return NULL;
    }
    return f;
}

/*************************************************************************/

/* Open a database file for reading (*mode == 'r') or writing (*mode == 'w').
 * Return the stream pointer, or NULL on error.  When opening for write, the
 * file actually opened is a temporary file, which will be renamed to the
 * original file on close.
 *
 * `version' is only used when opening a file for writing, and indicates the
 * version number to write to the file.
 */

dbFILE *open_db(const char *filename, const char *mode, int32 version)
{
    if (*mode == 'r') {
	return open_db_read(filename);
    } else if (*mode == 'w') {
	return open_db_write(filename, version);
    } else {
	errno = EINVAL;
	return NULL;
    }
}

/*************************************************************************/

/* Restore the database file to its condition before open_db().  This is
 * identical to close_db() for files open for reading; however, for files
 * open for writing, we discard the new temporary file instead of renaming
 * it over the old file.
 */

void restore_db(dbFILE *f)
{
    int errno_orig = errno;
    fclose(f->fp);
    if (f->mode == 'w' && *f->tempname)
	remove(f->tempname);
    free(f);
    errno = errno_orig;
}

/*************************************************************************/

/* Close a database file.  If the file was opened for write, moves the new
 * file over the old one, and logs/wallops an error message if the rename()
 * fails.
 */

int close_db(dbFILE *f)
{
    fclose(f->fp);
    if (f->mode=='w' && *f->tempname && strcmp(f->tempname,f->filename)!=0) {
	if (rename(f->tempname, f->filename) < 0) {
#ifndef CONVERT_DB
	    int errno_save = errno;
	    wallops(NULL, "Unable to move new data to database file %s;"
		    " new data NOT saved.", f->filename);
	    errno = errno_save;
	    module_log_perror("Unable to move new data to database file %s;"
			      " new data NOT saved.", f->filename);
#endif
	    remove(f->tempname);
	}
    }
    free(f);
    return 0;
}

/*************************************************************************/
/*************************************************************************/

/* Read and write 2- and 4-byte quantities, pointers, and strings.  All
 * multibyte values are stored in big-endian order (most significant byte
 * first).  A pointer is stored as a byte, either 0 if NULL or 1 if not,
 * and read pointers are returned as either (void *)0 or (void *)1.  A
 * string is stored with a 2-byte unsigned length (including the trailing
 * \0) first; a length of 0 indicates that the string pointer is NULL.
 * Written strings are truncated silently at 65534 bytes, and are always
 * null-terminated.
 *
 * All routines return -1 on error, 0 otherwise.
 */

/*************************************************************************/

int read_int8(uint8 *ret, dbFILE *f)
{
    int c = fgetc(f->fp);
    if (c == EOF)
	return -1;
    *ret = c;
    return 0;
}

int write_int8(uint8 val, dbFILE *f)
{
    if (fputc(val, f->fp) == EOF)
	return -1;
    return 0;
}

/*************************************************************************/

/* These two are inline to help out {read,write}_string. */

inline int read_int16(uint16 *ret, dbFILE *f)
{
    int c1, c2;

    c1 = fgetc(f->fp);
    c2 = fgetc(f->fp);
    if (c2 == EOF)
	return -1;
    *ret = c1<<8 | c2;
    return 0;
}

inline int write_int16(uint16 val, dbFILE *f)
{
    fputc((val>>8), f->fp);
    if (fputc(val, f->fp) == EOF)
	return -1;
    return 0;
}

/*************************************************************************/

int read_int32(uint32 *ret, dbFILE *f)
{
    int c1, c2, c3, c4;

    c1 = fgetc(f->fp);
    c2 = fgetc(f->fp);
    c3 = fgetc(f->fp);
    c4 = fgetc(f->fp);
    if (c4 == EOF)
	return -1;
    *ret = c1<<24 | c2<<16 | c3<<8 | c4;
    return 0;
}

int write_int32(uint32 val, dbFILE *f)
{
    fputc((val>>24), f->fp);
    fputc((val>>16), f->fp);
    fputc((val>> 8), f->fp);
    if (fputc((val & 0xFF), f->fp) == EOF)
	return -1;
    return 0;
}

/*************************************************************************/

int read_time(time_t *ret, dbFILE *f)
{
    int32 high, low;
    if (read_int32(&high, f) < 0 || read_int32(&low, f) < 0)
	return -1;
#if SIZEOF_TIME_T > 4
    *ret = (time_t)high << 32 | (time_t)low;
#else
    *ret = low;
#endif
    return 0;
}

int write_time(time_t val, dbFILE *f)
{
#if SIZEOF_TIME_T > 4
    if (write_int32(val>>32, f) < 0
     || write_int32(val & (time_t)0xFFFFFFFF, f) < 0)
#else
    if (write_int32(0, f) < 0 || write_int32(val, f) < 0)
#endif
	return -1;
    return 0;
}

/*************************************************************************/

int read_ptr(void **ret, dbFILE *f)
{
    int c;

    c = fgetc(f->fp);
    if (c == EOF)
	return -1;
    *ret = (c ? (void *)1 : (void *)0);
    return 0;
}

int write_ptr(const void *ptr, dbFILE *f)
{
    if (fputc(ptr ? 1 : 0, f->fp) == EOF)
	return -1;
    return 0;
}

/*************************************************************************/

int read_string(char **ret, dbFILE *f)
{
    char *s;
    uint16 len;

    if (read_int16(&len, f) < 0)
	return -1;
    if (len == 0) {
	*ret = NULL;
	return 0;
    }
    s = smalloc(len);
    if (len != fread(s, 1, len, f->fp)) {
	free(s);
	return -1;
    }
    *ret = s;
    return 0;
}

int write_string(const char *s, dbFILE *f)
{
    uint32 len;

    if (!s)
	return write_int16(0, f);
    len = strlen(s);
    if (len > 65534)
	len = 65534;
    write_int16((uint16)(len+1), f);
    fwrite(s, 1, len, f->fp);
    if (fputc(0, f->fp) == EOF)
	return -1;
    return 0;
}

/*************************************************************************/
/*************************************************************************/

#ifndef CONVERT_DB

void init_fileutil(Module *my_module)
{
    module = my_module;
}

#endif

/*************************************************************************/
