/* This module implements the relp frame object.
 *
 * Copyright 2008-2020 by Rainer Gerhards and Adiscon GmbH.
 *
 * This file is part of librelp.
 *
 * Librelp is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Librelp 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 Librelp.  If not, see <http://www.gnu.org/licenses/>.
 *
 * A copy of the GPL can be found in the file "COPYING" in this distribution.
 *
 * If the terms of the GPL are unsuitable for your needs, you may obtain
 * a commercial license from Adiscon. Contact sales@adiscon.com for further
 * details.
 *
 * ALL CONTRIBUTORS PLEASE NOTE that by sending contributions, you assign
 * your copyright to Adiscon GmbH, Germany. This is necessary to permit the
 * dual-licensing set forth here. Our apologies for this inconvenience, but
 * we sincerely believe that the dual-licensing model helps us provide great
 * free software while at the same time obtaining some funding for further
 * development.
 */
#include "config.h"
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <assert.h>
#include "relp.h"
#include "relpframe.h"

/** Construct a RELP frame instance
 */
static relpRetVal
relpFrameConstruct(relpFrame_t **ppThis, relpEngine_t *const pEngine)
{
	relpFrame_t *pThis;

	ENTER_RELPFUNC;
	assert(ppThis != NULL);
	if((pThis = calloc(1, sizeof(relpFrame_t))) == NULL) {
		ABORT_FINALIZE(RELP_RET_OUT_OF_MEMORY);
	}

	RELP_CORE_CONSTRUCTOR(pThis, Frame);
	pThis->pEngine = pEngine;

	*ppThis = pThis;

finalize_it:
	LEAVE_RELPFUNC;
}


/** Destruct a RELP frame instance
 */
relpRetVal
relpFrameDestruct(relpFrame_t **ppThis)
{
	relpFrame_t *pThis;

	ENTER_RELPFUNC;
	assert(ppThis != NULL);
	pThis = *ppThis;
	RELPOBJ_assert(pThis, Frame);

	if(pThis->pData != NULL)
		free(pThis->pData);

	/* done with de-init work, now free object itself */
	free(pThis);
	*ppThis = NULL;

	LEAVE_RELPFUNC;
}


/* Process a received octet.
 * This is a state machine. The transport driver needs to pass in octets received
 * one after the other. This function builds frames and submits them for processing
 * as need arises. Please note that the frame pointed to be ppThis may change during
 * processing. It may be NULL after a frame has fully be processed. On Init, the
 * caller can pass in a NULL pointer.
 * rgerhards, 2008-03-16
 */
relpRetVal LIBRELP_ATTR_NONNULL()
relpFrameProcessOctetRcvd(relpFrame_t **const ppThis,
	const relpOctet_t c,
	relpSess_t *const pSess)
{
	relpFrame_t *pThis;
	int frame_alloced = 0;
	ENTER_RELPFUNC;
	assert(ppThis != NULL);
	pThis = *ppThis;

	/* we allow NULL pointers, as we would not like to have unprocessed frame.
	 * Instead, a NULL frame pointer means that we have finished the previous frame
	 * (or did never before receive one) and so this character must be the first
	 * of a new frame. -- rgerhards, 2008-03-17
	 */
	if(pThis == NULL) {
		CHKRet(relpFrameConstruct(&pThis, pSess->pEngine));
		pThis->rcvState = eRelpFrameRcvState_BEGIN_FRAME;
		frame_alloced = 1;
	}

	switch(pThis->rcvState) {
		case eRelpFrameRcvState_BEGIN_FRAME:
			if(!isdigit(c))
				ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
			pThis->rcvState = eRelpFrameRcvState_IN_TXNR;
			/* FALLTHROUGH */
		case eRelpFrameRcvState_IN_TXNR:
			if(isdigit(c)) {
				if(pThis->iRcv++ == 9) { /* more than max permitted nbr of digits? */
					ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
				}
				pThis->txnr = pThis->txnr * 10 + c - '0';
			} else if(c == ' ') { /* field terminator */
				pThis->rcvState = eRelpFrameRcvState_IN_CMD;
				pThis->iRcv = 0;
			} else { /* oh, oh, invalid char! */
				ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
			}
			break;
		case eRelpFrameRcvState_IN_CMD:
			if(isalpha(c)) {
				if(pThis->iRcv == 32) { /* more than max permitted nbr of digits? */
					ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
				}
				pThis->cmd[pThis->iRcv] = c;
				++pThis->iRcv;
			} else if(c == ' ') { /* field terminator */
				pThis->cmd[pThis->iRcv] = '\0'; /* to make it suitable for strcmp() */
				pThis->rcvState = eRelpFrameRcvState_IN_DATALEN;
				pThis->iRcv = 0;
			} else { /* oh, oh, invalid char! */
				ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
			}
			break;
		case eRelpFrameRcvState_IN_DATALEN:
			if(isdigit(c)) {
				if(pThis->iRcv++ == 9) { /* more than max permitted nbr of digits? */
					ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
				}
				pThis->lenData = pThis->lenData * 10 + c - '0';
			} else {
				/* first check if we have no data, in which case
				 * we need to continue with the trailer check and skip
				 * the (non-present) data part. -- rgerhards, 2008-03-23
				 */
				if(pThis->lenData == 0) {
					pThis->rcvState = eRelpFrameRcvState_IN_TRAILER;
					pThis->iRcv = 0;
					goto do_trailer; /* not very clean, but... */
				}
				/* ok, we have data, so now let's do the usual checks... */
				if(c == ' ') { /* field terminator */
					pSess->maxCharsStore = pThis->lenData;
					if(pThis->lenData > pSess->maxDataSize) {
						if(pSess->pSrv == NULL) {
							/* we are a client */
							relpEngineCallOnGenericErr(pSess->pEngine,
								"librelp", RELP_RET_DATA_TOO_LONG,
								"server response frame too long, size %zu, "
								"configured max %zu -"
								"session will be closed (aborted)",
								pThis->lenData, pSess->maxDataSize);
							ABORT_FINALIZE(RELP_RET_DATA_TOO_LONG);
						}
						if(pSess->pSrv->oversizeMode == RELP_OVERSIZE_ABORT) {
							relpEngineCallOnGenericErr(pSess->pEngine,
								"librelp", RELP_RET_DATA_TOO_LONG,
								"frame too long, size %zu, configured max %zu -"
								"session will be closed (aborted)",
								pThis->lenData, pSess->maxDataSize);
							ABORT_FINALIZE(RELP_RET_DATA_TOO_LONG);
						} else if(pSess->pSrv->oversizeMode == RELP_OVERSIZE_TRUNCATE) {
							relpEngineCallOnGenericErr(pSess->pEngine,
								"librelp", RELP_RET_DATA_TOO_LONG,
								"frame too long, size %zu, configured max %zu -"
								"frame will be truncated, but session continues",
								pThis->lenData, pSess->maxDataSize);
							pSess->maxCharsStore = pSess->maxDataSize;
						} else if(pSess->pSrv->oversizeMode == RELP_OVERSIZE_ACCEPT) {
							relpEngineCallOnGenericErr(pSess->pEngine,
								"librelp", RELP_RET_DATA_TOO_LONG,
								"frame too long, size %zu, configured max %zu -"
								"frame will still be accepted and session "
								"continues. Note that this can be caused by an "
								"attack on your system.",
								pThis->lenData, pSess->maxDataSize);
						} else {
							relpEngineCallOnGenericErr(pSess->pEngine,
								"librelp", RELP_RET_ERR_INTERNAL,
								"librelp error: invalid oversizeMode in %s:%d "
								"mode currently is %d - session aborted",
								__FILE__, __LINE__, pSess->pSrv->oversizeMode);
							assert(0); /* ensure termination in debug mode! */
						}
					}
					/* we now can assign the buffer for our data */
					if(pThis->lenData > 0) {
						if((pThis->pData = malloc(pSess->maxCharsStore)) == NULL)
							ABORT_FINALIZE(RELP_RET_OUT_OF_MEMORY);
					}
					pThis->rcvState = eRelpFrameRcvState_IN_DATA;
					pThis->iRcv = 0;
				} else { /* oh, oh, invalid char! */
					ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
				}
			}
			break;
		case eRelpFrameRcvState_IN_DATA:
			if(pThis->iRcv < pThis->lenData) {
				if(pThis->iRcv < pSess->maxCharsStore) {
					pThis->pData[pThis->iRcv] = c;
				}
				++pThis->iRcv;
				break;
			} else { /* end of data */
				if(pSess->maxCharsStore < pThis->lenData) {
					pThis->lenData = pSess->maxCharsStore;
				}
				pThis->rcvState = eRelpFrameRcvState_IN_TRAILER;
				pThis->iRcv = 0;
			}
			/*FALLTHROUGH*/
		case eRelpFrameRcvState_IN_TRAILER:
		do_trailer:
			if(c != '\n')
				ABORT_FINALIZE(RELP_RET_INVALID_FRAME);
			pThis->rcvState = eRelpFrameRcvState_FINISHED;
			/* submit frame to processor */
			/* NOTE: do not abort in any case, because we need to destruct the frame! */
			if(pThis->txnr != 0 && pSess->sessType == eRelpSess_Server) {
				if(pThis->txnr != pSess->txnr)  {
					iRet = RELP_RET_INVALID_TXNR;
				} else {
					pSess->txnr = relpEngineNextTXNR(pSess->txnr);
				}
			}
			if(iRet == RELP_RET_OK) {
				iRet = relpEngineDispatchFrame(pSess->pEngine, pSess, pThis);
			}
			relpFrameDestruct(&pThis);
			break;
		case eRelpFrameRcvState_FINISHED:
			assert(0); /* this shall never happen */
			break;
		default:assert(0); /* make sure we die when debugging */
			relpEngineCallOnGenericErr(pSess->pEngine,
				"librelp", RELP_RET_ERR_INTERNAL,
				"invalid FrameRcvState %d in %s:%d",
				pThis->rcvState, __FILE__, __LINE__);
			break;
	}

	*ppThis = pThis;

finalize_it:
// TODO:more cleanup
	if(iRet != RELP_RET_OK) {
		if(frame_alloced) {
			relpFrameDestruct(&pThis);
		}
	}
	LEAVE_RELPFUNC;
}


/* Update an already-existing Sendbuf with a new txnr. This function is needed
 * during recovery of failed connections where a queue of unsent messages
 * exists.
 * rgerhards, 2008-03-23
 */
relpRetVal
relpFrameRewriteTxnr(relpSendbuf_t *pSendbuf, relpTxnr_t txnr)
{
	char bufTxnr[16];
	size_t lenTxnr;
	relpOctet_t *ptrMembuf;

	ENTER_RELPFUNC;
	RELPOBJ_assert(pSendbuf, Sendbuf);
	
	pSendbuf->txnr = txnr;

	lenTxnr = snprintf(bufTxnr, sizeof(bufTxnr), "%d", (int) txnr);
	if(lenTxnr > 9)
		ABORT_FINALIZE(RELP_RET_INVALID_TXNR);

	/* we have always enough space inside the frame to patch in the new
	 * txnr - it was reserved during initial frame creation. So now patch it...
	 */
	ptrMembuf = pSendbuf->pData + 9 - lenTxnr; /* set ptr to start of area we intend to write to */
	pSendbuf->lenData = pSendbuf->lenData - pSendbuf->lenTxnr + lenTxnr;
	pSendbuf->lenTxnr = lenTxnr;
	memcpy(ptrMembuf, bufTxnr, lenTxnr);

	/* the rest of the frame remains unchanged */

finalize_it:
	LEAVE_RELPFUNC;
}


/* create a relpSendbuf_t with a valid relp frame. While this is a frame object
 * function, it does not accept the frame object itself but rather the individual
 * frame parameters. The reason is that we want to save the overhead of constructing
 * and desructing a frame object in cases where the frame object itself is not
 * needed (e.g. when sending a "rsp" frame). This method
 * is meant to be invoked immediately before sending a frame. So the txnr must
 * be passed in, as it is at the top of the frame. Note that it should only be called
 * from within a mutex-protected region that ensures the txnr remains
 * consistent. The sendbuf is allocated inside this function
 * and a pointer to it passed to the caller. The caller is responsible
 * for destructing it. -- rgerhards, 2008-03-19
 */
relpRetVal
relpFrameBuildSendbuf(relpSendbuf_t **ppSendbuf, relpTxnr_t txnr, unsigned char *pCmd, const size_t lenCmd,
		      relpOctet_t *pData, size_t lenData, relpSess_t *pSess,
		      relpRetVal (*rspHdlr)(relpSess_t*, relpFrame_t*))
{
	char bufTxnr[16];
	size_t lenTxnr;
	char bufDatalen[16];
	size_t lenDatalen;
	relpSendbuf_t *pSendbuf = NULL;
	relpOctet_t *ptrMembuf;

	ENTER_RELPFUNC;
	assert(ppSendbuf != NULL);
	
	CHKRet(relpSendbufConstruct(&pSendbuf, pSess));
	pSendbuf->txnr = txnr;
	pSendbuf->rspHdlr = rspHdlr;

	lenTxnr = snprintf(bufTxnr, sizeof(bufTxnr), "%d", (int) txnr);
	if(lenTxnr > 9)
		ABORT_FINALIZE(RELP_RET_INVALID_TXNR);

	lenDatalen = snprintf(bufDatalen, sizeof(bufDatalen), "%d", (int) lenData);
	if(lenDatalen > 9)
		ABORT_FINALIZE(RELP_RET_INVALID_DATALEN);
	
	/* we got everything, so now let's get our membuf [+1 for SP's and TRAILER]
	 * We do a trick in our calculation: we always assign 9 bytes for the txnr, even
	 * though it may be less than that. The reason is that on a sessin recovery case,
	 * we can put another, potentially longer, txnr into the frame. This saves us from
	 * copying the while frame just to adjust the txnr. -- rgerhards, 2008-03-23
	 */
	pSendbuf->lenData = lenTxnr + 1 + lenCmd + 1 + lenDatalen + 1;
	if(lenData > 0)
		pSendbuf->lenData += 1 + lenData;

if((pSendbuf->pData = malloc(pSendbuf->lenData + (9 - lenTxnr) + 1)) == NULL)
	// remove +1 above (for debugging only!)
		ABORT_FINALIZE(RELP_RET_OUT_OF_MEMORY);

	ptrMembuf = pSendbuf->pData + 9 - lenTxnr; /* set ptr to start of area we intend to write to */
	pSendbuf->lenTxnr = lenTxnr;

	memcpy(ptrMembuf, bufTxnr, lenTxnr); ptrMembuf += lenTxnr;
	*ptrMembuf++ = ' ';
	memcpy(ptrMembuf, pCmd, lenCmd); ptrMembuf += lenCmd;
	*ptrMembuf++ = ' ';
	memcpy(ptrMembuf, bufDatalen, lenDatalen); ptrMembuf += lenDatalen;
	if(lenData > 0) {
		/* a data part (and its SP-delemiter) is only written if there is data... */
		*ptrMembuf++ = ' ';
		memcpy(ptrMembuf, pData, lenData); ptrMembuf += lenData;
	}

	*ptrMembuf = '\n';
*++ptrMembuf = '\0'; /* just for  the dbgprint below */

	*ppSendbuf = pSendbuf; /* save new buffer */

finalize_it:
	if(iRet != RELP_RET_OK) {
		if(pSendbuf != NULL)
			relpSendbufDestruct(&pSendbuf);
	}

	LEAVE_RELPFUNC;
}


/* return the next character from the framebuf or indicate end of buffer (-1).
 * rgerhards, 2008-03-24
 */
relpRetVal
relpFrameGetNextC(relpFrame_t *const pThis, unsigned char *const pC)
{
	ENTER_RELPFUNC;
	RELPOBJ_assert(pThis, Frame);
	assert(pC != NULL);

	if(pThis->idxData >= pThis->lenData)
		iRet = RELP_RET_END_OF_DATA;
	else
		*pC = pThis->pData[pThis->idxData++];

	LEAVE_RELPFUNC;
}
