/* Synchronet message base (SMB) alloc/free routines */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This library is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU Lesser General Public License		*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU Lesser General Public License for more details: lgpl.txt or	*
 * http://www.fsf.org/copyleft/lesser.html									*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "smblib.h"
#include "genwrap.h"

/****************************************************************************/
/* Finds unused space in data file based on block allocation table and		*/
/* marks space as used in allocation table.                                 */
/* File must be opened read/write DENY ALL									*/
/* Returns offset to beginning of data (in bytes, not blocks)				*/
/* Assumes smb_open_da() has been called									*/
/* smb_close_da() should be called after									*/
/* Returns negative on error												*/
/****************************************************************************/
off_t smb_allocdat(smb_t* smb, off_t length, uint16_t refs)
{
	uint16_t i;
	uint     j, l, blocks;
	off_t    offset = 0;

	if (smb->sda_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	blocks = smb_datblocks(length);
	j = 0;    /* j is consecutive unused block counter */
	fflush(smb->sda_fp);
	rewind(smb->sda_fp);
	while (!feof(smb->sda_fp) && (int)offset >= 0) {
		if (smb_fread(smb, &i, sizeof(i), smb->sda_fp) != sizeof(i))
			break;
		offset += SDT_BLOCK_LEN;
		if (!i)
			j++;
		else
			j = 0;
		if (j == blocks) {
			offset -= (blocks * SDT_BLOCK_LEN);
			break;
		}
	}
	if ((int)offset < 0) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s invalid data offset: %" PRIdOFF, __FUNCTION__, offset);
		return SMB_ERR_DAT_OFFSET;
	}
	clearerr(smb->sda_fp);
	if (fseeko(smb->sda_fp, (offset / SDT_BLOCK_LEN) * sizeof(refs), SEEK_SET)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s seeking to: %" PRIdOFF, __FUNCTION__
		              , (offset / SDT_BLOCK_LEN) * sizeof(refs));
		return SMB_ERR_SEEK;
	}
	for (l = 0; l < blocks; l++)
		if (!fwrite(&refs, sizeof(refs), 1, smb->sda_fp)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation bytes at offset %" PRIdOFF, __FUNCTION__
			              , ((offset / SDT_BLOCK_LEN) + l) * sizeof(refs));
			return SMB_ERR_WRITE;
		}
	fflush(smb->sda_fp);
	return offset;
}

/****************************************************************************/
/* Allocates space for data, but doesn't search for unused blocks           */
/* Returns negative on error												*/
/****************************************************************************/
off_t smb_fallocdat(smb_t* smb, off_t length, uint16_t refs)
{
	uint  l, blocks;
	off_t offset;

	if (smb->sda_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	fflush(smb->sda_fp);
	clearerr(smb->sda_fp);
	blocks = smb_datblocks(length);
	if (fseek(smb->sda_fp, 0L, SEEK_END)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s rewinding", __FUNCTION__);
		return SMB_ERR_SEEK;
	}
	offset = (ftell(smb->sda_fp) / sizeof(refs)) * SDT_BLOCK_LEN;
	if ((int)offset < 0) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error)
		              , "%s invalid data offset: %" PRIdOFF, __FUNCTION__, offset);
		return SMB_ERR_DAT_OFFSET;
	}
	for (l = 0; l < blocks; l++)
		if (!fwrite(&refs, sizeof(refs), 1, smb->sda_fp))
			break;
	fflush(smb->sda_fp);
	if (l < blocks) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error)
		              , "%s writing allocation bytes", __FUNCTION__);
		return SMB_ERR_WRITE;
	}
	return offset;
}

/****************************************************************************/
/* De-allocates space for data												*/
/* Returns non-zero on error												*/
/* Always unlocks the SMB header (when not hyper-alloc)						*/
/****************************************************************************/
int smb_freemsgdat(smb_t* smb, off_t offset, uint length, uint16_t refs)
{
	bool     da_opened = false;
	int      retval = SMB_SUCCESS;
	uint16_t i;
	int      l, blocks;
	off_t    sda_offset;
	off_t    flen;

	if (offset < 0)
		return SMB_ERR_DAT_OFFSET;

	if (smb->status.attr & SMB_HYPERALLOC) /* do nothing */
		return SMB_SUCCESS;

	blocks = smb_datblocks(length);

	if (blocks < 1)
		return SMB_SUCCESS; // Nothing to do

	if (smb->sda_fp == NULL) {
		if ((i = smb_open_da(smb)) != SMB_SUCCESS)
			return i;
		da_opened = true;
	}
	flen = filelength(fileno(smb->sda_fp));
	if (flen < sizeof(uint16_t))
		return 0;   // Nothing to do

	if (!smb->locked && smb_locksmbhdr(smb) != SMB_SUCCESS)
		return SMB_ERR_LOCK;

	clearerr(smb->sda_fp);
	// Free from the last block first
	for (l = blocks - 1; l >= 0; l--) {
		sda_offset = ((offset / SDT_BLOCK_LEN) + l) * sizeof(i);
		if (fseeko(smb->sda_fp, sda_offset, SEEK_SET)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s %d '%s' seeking to %" PRIdOFF " of allocation file", __FUNCTION__
			              , get_errno(), strerror(get_errno())
			              , sda_offset);
			retval = SMB_ERR_SEEK;
			break;
		}
		if (smb_fread(smb, &i, sizeof(i), smb->sda_fp) != sizeof(i)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s reading allocation record at offset %" PRIdOFF, __FUNCTION__
			              , sda_offset);
			retval = SMB_ERR_READ;
			break;
		}
		if (refs == SMB_ALL_REFS || refs > i)
			i = 0;          /* don't want to go negative */
		else
			i -= refs;

		// Completely free? and at end of SDA? Just truncate record from end of file
		if (i == 0 && ftell(smb->sda_fp) == flen) {
			if (chsize(fileno(smb->sda_fp), (int)sda_offset) == 0) {
				flen = sda_offset;
				continue;
			}
		}

		if (fseek(smb->sda_fp, -(int)sizeof(i), SEEK_CUR)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s %d '%s' seeking backwards 2 bytes in allocation file", __FUNCTION__
			              , get_errno(), strerror(get_errno()));
			retval = SMB_ERR_SEEK;
			break;
		}
		if (!fwrite(&i, sizeof(i), 1, smb->sda_fp)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation bytes at offset %" PRIdOFF, __FUNCTION__
			              , sda_offset);
			retval = SMB_ERR_WRITE;
			break;
		}
	}
	fflush(smb->sda_fp);
	if (filelength(fileno(smb->sdt_fp)) / SDT_BLOCK_LEN > (int)(filelength(fileno(smb->sda_fp)) / sizeof(uint16_t)))
		if (chsize(fileno(smb->sdt_fp), (int)(filelength(fileno(smb->sda_fp)) / sizeof(uint16_t)) * SDT_BLOCK_LEN) != 0)
			retval = SMB_ERR_TRUNCATE;
	if (da_opened)
		smb_close_da(smb);
	smb_unlocksmbhdr(smb);
	return retval;
}

/****************************************************************************/
/* Adds to data allocation records for blocks starting at 'offset'          */
/* SMB header should be locked before calling this function					*/
/* Returns non-zero on error												*/
/****************************************************************************/
int smb_incdat(smb_t* smb, off_t offset, uint length, uint16_t refs)
{
	uint16_t i;
	uint     l, blocks;

	if (smb->sda_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	clearerr(smb->sda_fp);
	blocks = smb_datblocks(length);
	for (l = 0; l < blocks; l++) {
		if (fseeko(smb->sda_fp, ((offset / SDT_BLOCK_LEN) + l) * sizeof(i), SEEK_SET)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s seeking to %" PRIdOFF, __FUNCTION__
			              , ((offset / SDT_BLOCK_LEN) + l) * sizeof(i));
			return SMB_ERR_SEEK;
		}
		if (smb_fread(smb, &i, sizeof(i), smb->sda_fp) != sizeof(i)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s reading allocation record at offset %" PRIdOFF, __FUNCTION__
			              , ((offset / SDT_BLOCK_LEN) + l) * sizeof(i));
			return SMB_ERR_READ;
		}
		i += refs;
		if (fseek(smb->sda_fp, -(int)sizeof(i), SEEK_CUR)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s rewinding %d", __FUNCTION__, -(int)sizeof(i));
			return SMB_ERR_SEEK;
		}
		if (!fwrite(&i, sizeof(i), 1, smb->sda_fp)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation record at offset %" PRIdOFF, __FUNCTION__
			              , ((offset / SDT_BLOCK_LEN) + l) * sizeof(i));
			return SMB_ERR_WRITE;
		}
	}
	return fflush(smb->sda_fp); /* SMB_SUCCESS == 0 */
}

/****************************************************************************/
/* Increments data allocation records (message references) by number of		*/
/* header references specified (usually 1)									*/
/* The opposite function of smb_freemsg()									*/
/* Always unlocks the SMB header (when not hyper-alloc)						*/
/****************************************************************************/
int smb_incmsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
{
	int      i = SMB_SUCCESS;
	bool     da_opened = false;
	uint16_t x;

	if (smb->status.attr & SMB_HYPERALLOC)  /* Nothing to do */
		return SMB_SUCCESS;

	if (smb->sda_fp == NULL) {
		if ((i = smb_open_da(smb)) != SMB_SUCCESS)
			return i;
		da_opened = true;
	}

	if (!smb->locked && smb_locksmbhdr(smb) != SMB_SUCCESS)
		return SMB_ERR_LOCK;

	for (x = 0; x < msg->hdr.total_dfields; x++) {
		if ((i = smb_incdat(smb, msg->hdr.offset + msg->dfield[x].offset
		                    , msg->dfield[x].length, refs)) != SMB_SUCCESS)
			break;
	}
	smb_unlocksmbhdr(smb);

	if (da_opened)
		smb_close_da(smb);

	return i;
}

/****************************************************************************/
/* De-allocates blocks for header record									*/
/* Returns non-zero on error												*/
/****************************************************************************/
int smb_freemsghdr(smb_t* smb, off_t offset, uint length)
{
	uchar c = 0;
	int   l, blocks;
	off_t sha_offset;

	if (smb->status.attr & SMB_HYPERALLOC)  /* Nothing to do */
		return SMB_SUCCESS;

	if (smb->sha_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	clearerr(smb->sha_fp);
	blocks = smb_hdrblocks(length);
	if (blocks < 1) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s invalid header length: %u", __FUNCTION__, length);
		return SMB_ERR_HDR_LEN;
	}

	sha_offset = offset / SHD_BLOCK_LEN;
	if (filelength(fileno(smb->sha_fp)) <= (sha_offset + blocks)) {
		if (chsize(fileno(smb->sha_fp), (int)sha_offset) == 0) {
			if (chsize(fileno(smb->shd_fp), (int)(smb->status.header_offset + offset)) != 0) {
				safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s header truncation failure", __FUNCTION__);
				return SMB_ERR_TRUNCATE;
			}
			return SMB_SUCCESS;
		}
	}

	if (fseeko(smb->sha_fp, sha_offset, SEEK_SET)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s seeking to %d", __FUNCTION__, (int)sha_offset);
		return SMB_ERR_SEEK;
	}
	for (l = 0; l < blocks; l++)
		if (!fwrite(&c, 1, 1, smb->sha_fp)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation record", __FUNCTION__);
			return SMB_ERR_WRITE;
		}
	return fflush(smb->sha_fp); /* SMB_SUCCESS == 0 */
}

/****************************************************************************/
/****************************************************************************/
int smb_freemsg_dfields(smb_t* smb, smbmsg_t* msg, uint16_t refs)
{
	int      i;
	uint16_t x;

	for (x = 0; x < msg->hdr.total_dfields; x++) {
		if ((i = smb_freemsgdat(smb, msg->hdr.offset + msg->dfield[x].offset
		                        , msg->dfield[x].length, refs)) != SMB_SUCCESS)
			return i;
	}
	return SMB_SUCCESS;
}

/****************************************************************************/
/* Frees all allocated header and data blocks (1 reference) for 'msg'       */
/****************************************************************************/
int smb_freemsg(smb_t* smb, smbmsg_t* msg)
{
	int i;

	if (smb->status.attr & SMB_HYPERALLOC)  /* Nothing to do */
		return SMB_SUCCESS;

	if (!smb_valid_hdr_offset(smb, msg->idx.offset))
		return SMB_ERR_HDR_OFFSET;

	if ((i = smb_freemsg_dfields(smb, msg, 1)) != SMB_SUCCESS)
		return i;

	return smb_freemsghdr(smb, msg->idx.offset - smb->status.header_offset
	                      , msg->hdr.length);
}

/****************************************************************************/
/* Finds unused space in header file based on block allocation table and	*/
/* marks space as used in allocation table.                                 */
/* File must be opened read/write DENY ALL									*/
/* Returns offset to beginning of header (in bytes, not blocks) 			*/
/* Assumes smb_open_ha() has been called									*/
/* smb_close_ha() should be called after									*/
/* Returns negative value on error 											*/
/****************************************************************************/
off_t smb_allochdr(smb_t* smb, uint length)
{
	uchar c;
	uint  i, l, blocks, offset = 0;

	if (smb->sha_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	blocks = smb_hdrblocks(length);
	i = 0;    /* i is consecutive unused block counter */
	fflush(smb->sha_fp);
	rewind(smb->sha_fp);
	while (!feof(smb->sha_fp)) {
		if (smb_fread(smb, &c, sizeof(c), smb->sha_fp) != sizeof(c))
			break;
		offset += SHD_BLOCK_LEN;
		if (!c)
			i++;
		else
			i = 0;
		if (i == blocks) {
			offset -= (blocks * SHD_BLOCK_LEN);
			break;
		}
	}
	clearerr(smb->sha_fp);
	if (fseek(smb->sha_fp, offset / SHD_BLOCK_LEN, SEEK_SET)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s seeking to %d", __FUNCTION__, offset / SHD_BLOCK_LEN);
		return SMB_ERR_SEEK;
	}
	for (l = 0; l < blocks; l++)
		if (fputc(1, smb->sha_fp) != 1) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation record", __FUNCTION__);
			return SMB_ERR_WRITE;
		}
	fflush(smb->sha_fp);
	return offset;
}

/****************************************************************************/
/* Allocates space for header, but doesn't search for unused blocks          */
/* Returns negative value on error 											*/
/****************************************************************************/
off_t smb_fallochdr(smb_t* smb, uint length)
{
	uchar c = 1;
	uint  l, blocks, offset;

	if (smb->sha_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	blocks = smb_hdrblocks(length);
	fflush(smb->sha_fp);
	clearerr(smb->sha_fp);
	if (fseek(smb->sha_fp, 0L, SEEK_END)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s rewinding", __FUNCTION__);
		return SMB_ERR_SEEK;
	}
	offset = ftell(smb->sha_fp) * SHD_BLOCK_LEN;
	for (l = 0; l < blocks; l++)
		if (!fwrite(&c, 1, 1, smb->sha_fp)) {
			safe_snprintf(smb->last_error, sizeof(smb->last_error)
			              , "%s writing allocation record", __FUNCTION__);
			return SMB_ERR_WRITE;
		}
	fflush(smb->sha_fp);
	return offset;
}

/************************************************************************/
/* Allocate header blocks using Hyper Allocation						*/
/* this function should be most likely not be called from anywhere but	*/
/* smb_addmsghdr()														*/
/************************************************************************/
off_t smb_hallochdr(smb_t* smb)
{
	uint offset;

	if (smb->shd_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	fflush(smb->shd_fp);
	if (fseek(smb->shd_fp, 0L, SEEK_END)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s rewinding", __FUNCTION__);
		return SMB_ERR_SEEK;
	}
	offset = ftell(smb->shd_fp);
	if (offset < smb->status.header_offset)    /* Header file truncated?!? */
		return smb->status.header_offset;

	offset -= smb->status.header_offset;      /* SMB headers not included */

	/* Even block boundry */
	offset += PAD_LENGTH_FOR_ALIGNMENT(offset, SHD_BLOCK_LEN);

	return offset;
}

/************************************************************************/
/* Allocate data blocks using Hyper Allocation							*/
/* smb_locksmbhdr() should be called before this function and not		*/
/* unlocked until all data fields for this message have been written	*/
/* to the SDT file														*/
/************************************************************************/
off_t smb_hallocdat(smb_t* smb)
{
	off_t offset;

	if (smb->sdt_fp == NULL) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error)
		              , "%s msgbase not open", __FUNCTION__);
		return SMB_ERR_NOT_OPEN;
	}
	fflush(smb->sdt_fp);
	offset = filelength(fileno(smb->sdt_fp));
	if (offset < 0) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error)
		              , "%s invalid file length: %" PRIdOFF, __FUNCTION__, offset);
		return SMB_ERR_FILE_LEN;
	}
	if (fseek(smb->sdt_fp, 0L, SEEK_END)) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s rewinding", __FUNCTION__);
		return SMB_ERR_SEEK;
	}
	offset = ftell(smb->sdt_fp);
	if (offset < 0) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error)
		              , "%s invalid file offset: %" PRIdOFF, __FUNCTION__, offset);
		return SMB_ERR_DAT_OFFSET;
	}

	/* Make sure even block boundry */
	offset += PAD_LENGTH_FOR_ALIGNMENT(offset, SDT_BLOCK_LEN);

	return offset;
}