/* 
   Unix SMB/Netbios implementation.
   Version 1.9.
   Copyright (C) Andrew Tridgell 1994-1997

   This file is part of the port to OpenVMS
   Stream read and open routines.
   Copyright (C) Eckart Meyer 1996-1997
   
   This program 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 2 of the License, 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; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*	stm_read.c
 *	V1.0			23-Jul-1995
 *
 *	Copyright E. Meyer <meyer@ifn.ing.tu-bs.de>
 *+
 * This module contains replacement routines for OpenVMS file handling
 * routines
 *
 *	open, close, read, lseek
 *
 * to read record oriented (text) files as if they were stream files
 * (CR and LF as record terminators), maintaining the stream position.
 *
 * The record terminators are appended to each record read from VMS.
 * 'lseek' returns the stream position (counting all record bytes plus
 * the terminator bytes). Thus, seeking means reading the file and
 * performance may be bad. Seeking backwards even means rewinding and
 * reading from the beginning.
 *-
 */

/*
 * in case the original routines have been previously redirected,
 * we call those by defining names preceded by an underscore.
 * This is usefull if e.g. read and close are redirected to a socket
 * library like SOCKETSHR.
 */
#ifdef _open
#define open _open
#endif
#ifdef _read
#define read _read
#endif
#ifdef _close
#define close _close
#endif
#ifdef _lseek
#define lseek _lseek
#endif

#define const
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stat.h>
#if __DECC_VER < 50200000  ||  __VMS_VER < 70000000
#include <unixio.h>
#endif
#include <string.h>
#include <fcntl.h>
#include <errno.h>

#ifndef TRUE
#define TRUE 1
#endif

#ifndef FALSE
#define FALSE 0
#endif

typedef short int BOOL;

#if 1	/* for use with samba's debugging */
extern int DEBUGLEVEL;
#define DEBUG(level,body) ((DEBUGLEVEL>=(level))?(Debug1 body):0)
int Debug1();
#else
int DEBUGLEVEL = 8;
#define DEBUG(level,body) ((DEBUGLEVEL>=(level))?(printf body):0)
#endif

/*
 * if read() should fill up with 0 until the file size returned by stat()
 *  has been returned, define FILL_WITH_NULL. Or use FILL_WITH_CTRLZ
 */
/* #define FILL_WITH_NULL */
#define FILL_WITH_CTRLZ

char *VMSMakeFilename(char * __vms_buf, char * __file_spec); /* SAMBA */
char *vms_make_name(char * __NAME);	/* SAMBA */
int vms_stat(const char *__path, struct stat *__buf); /* SAMBA */

/*
 * Foward declarations
 */
int stm_read(int __fd, void * __buf, unsigned int __maxlen);
long stm_lseek(int __fd, long __offset, int __origin);
int stm_open(char * __fname, int __flags, int __mode);
int stm_close(int __fd);

static long is_textfile(char * __name);

/*
 * Define our internal file descriptor table.
 */
struct fd_entry {
	int fd;				/* the file descriptor */
	long stream_pos;		/* bytes read (including rec. term.) */
	long rec_pos;			/* record position returned by lseek */
	long rec_offset;		/* offset into next record */
	int pending;			/* number of bytes pending */
	long size;			/* file size from stat() */
	BOOL record_mode;		/* record mode flag */
	char pending_buf[2];		/* record terminator */
};

/*
 * Define the number of fd entries allocated. If all entries are used
 * the table must be enlarged by another chunk of entries.
 */
#define FD_CHUNK 10
static struct fd_entry *fds = NULL;
static int fd_tabsize = 0;

/*
 * Define the size of the read buffer for dummy reads in lseek.
 * This buffer is allocated dynamically for each lseek.
 */
#define SEEK_BUFFER 2048



/* Given a file descriptor, return the fd table entry if any */

static struct fd_entry *find_fd_entry(fd)
{
	struct fd_entry *pf;
	int i;

	if (fds == NULL) return(NULL);
	for(i = 0, pf = fds; i < fd_tabsize; i++, pf++) {
		if (pf->fd == fd) return(pf);
	}
	return(NULL);
}


/* allocate a new chunk of fd entries. */

static int allocate_fd_entries(int num)
{
	struct fd_entry *pf;
	int i;
/*
 * Allocate or reallocate (enlarge) the table.
 */	
	i = (fd_tabsize + num) * sizeof(struct fd_entry);
	if (fds == NULL)
		pf = (struct fd_entry *)malloc(i);
	else
		pf = (struct fd_entry *)realloc(fds,i);
	if (pf == NULL) {
		DEBUG (0,("stm_open: can't allocate memory for fd table\n"));
		errno = ENOMEM;
		return(-1);
	}
/*
 * Save new pointer. Set new entries to 'unused'.
 */
	fds = pf;
	for(i = fd_tabsize; i < fd_tabsize + num; i++) {
		fds[i].fd = -1;
	}
	fd_tabsize = fd_tabsize + num;
	return(0);
}


/*
 * open
 *
 * Check the access type and file type.
 * If a file of type 'variable record' and 'carriage control' we
 * assume a text file and enable record to stream conversion.
 */

int stm_open(char *fname, int flags, int mode)
{
	long sts;
	struct stat st;
	struct fd_entry *pf;
	int fd;
	int i;
	char fname1[256];
	int is_printfile = FALSE;

/*	strcpy(fname1, vms_make_name(fname));	/**/
	VMSMakeFilename(fname1, fname);		/**/
        DEBUG(3,("Open file \"%s\"  ->  \"%s\"\n",fname,fname1));
	DEBUG(3,("---flags: %08X, mode: %06o\n",flags,mode));
	if (flags & 0x40000000) {
		flags &= ~0x40000000;
		is_printfile = TRUE;
	}
/*
 * We only handle reading of files.
 * Clear possible entry in fd table.
 */
	if (flags & O_CREAT) {
		if (is_printfile) {
			fd = open(fname1,flags,mode,"rfm=fix","mrs=512","ctx=bin");
		}
		else {
			fd = open(fname1,flags,mode,"rfm=stm","ctx=stm","ctx=bin");
		}
		pf = find_fd_entry(fd);
		if (pf != NULL) pf->fd = -1;	/* invalidate */
		return(fd);
	}
/*
 * Check the VMS file attributes. Record type 'variable' and
 * attribute 'carriage control' means we should use record mode.
 */
	else {
		sts = stat(fname1,&st);
		if (sts != 0) return(-1);
		if (st.st_mode & S_IFDIR) {
			errno = EISDIR;
DEBUG(3,("--- S_IFDIR: errno: %d\n",errno));
			return(-1);
		}
		if ( (i = is_textfile(fname1)) <= 0 ) {
			fd = open(fname1,flags,mode,"rfm=stm","ctx=stm","ctx=bin");
			DEBUG(3,("--- fd: %d, errno: %d, vms$err: %08X\n",fd,errno,vaxc$errno));
			pf = find_fd_entry(fd);
			if (pf != NULL) pf->fd = -1;	/* invalidate */
			return(fd);
		}
	}
/*
 * The file should be read in record mode.
 * Try to find an empty entry in fd table. If no, allocate (or enlarge)
 * memory for FD_CHUNK new entries.
 */
	pf = find_fd_entry(-1);		/* find empty entry */
	if (pf == NULL) {
		sts = allocate_fd_entries(FD_CHUNK);
		if (sts < 0) return(sts);
		pf = find_fd_entry(-1);		/* find empty entry */
	}
/*
 * Open the file in binary record mode, i.e. return the bytes of a record
 * without any record terminator (normally a LF would be appended). We will
 * add out own record terminators.
 *
 * Initialize our table entry.
 */
	DEBUG(3,("Open file %s in record mode\n",fname1));
	fd = open(fname1,flags,mode,"ctx=rec");
	if (fd < 0) return(fd);

	pf->fd = fd;
	pf->size = i;
	pf->record_mode = TRUE;
	pf->stream_pos = 0;
	pf->rec_pos = 0;
	pf->rec_offset = 0;
	pf->pending_buf[0] = '\r';	/* This is the CR and ... */
	pf->pending_buf[1] = '\n';	/* ... LF record terminator */
	pf->pending = 0;
	return(fd);
}


/*
 * close
 *
 * Just do it. And mark a possible fd table entry unused.
 */

int stm_close(int fd)
{
	struct fd_entry *pf;

	if (fd < 0) {
		errno = EBADF;
		return(-1);
	}
	pf = find_fd_entry(fd);
	if (pf != NULL) pf->fd = -1;
	return(close(fd));
}


/*
 * read
 *
 * Read the records from file and append the record termination bytes.
 * Maintain the stream position (pure bytes of the record plus termination
 * bytes).
 */

int stm_read(int fd, void *buf, unsigned int maxlen)
{
	struct fd_entry *pf;
	int total_ret = 0;
	int ret = 0;
	int i;
	int pos;

	if (fd < 0) {
		errno = EBADF;
		return(-1);
	}
	if (buf == NULL) return(0);
	if (maxlen == 0) return(0);
	pf = find_fd_entry(fd);
	if (pf == NULL || (!pf->record_mode)) return(read(fd, buf, maxlen));
/*
 * If there are record terminator bytes pending which did not fit in
 * the buffer of a previous read, try to return them now.
 */
	if (pf->pending) {
		ret = (maxlen >= pf->pending) ? pf->pending : maxlen;
		memcpy(buf, pf->pending_buf + 2 - pf->pending, ret);
		total_ret += ret;
		buf = (char *)buf + ret;
		maxlen -= ret;
		pf->pending -= ret;
	}
/*
 * Now read records.
 * Each read does not read beyond the record boundary. If the last byte
 * of a record is read, the record position returned by lseek is updated.
 * It is not changed otherwise. Thus we maintain the record byte offset
 * ourself.
 */
	while (maxlen > 0) {
		ret = read(fd, buf, maxlen);
		if (ret == 0) break;		/* end of file */
		if (ret < 0) return(ret);	/* error */
		pos = lseek(fd, 0, SEEK_CUR);
/*
 * If the end of a record is reached, we try to additionally return
 * the record terminator bytes. If the buffer is too small for the extra
 * bytes, remember them for the next call.
 */
		if (pos != pf->rec_pos) {	/* end of record */
			--ret;			/* discard the LF */
			i = ( (maxlen-ret) >= 2) ? 2 : maxlen-ret;
			memcpy( (char *)buf + ret, pf->pending_buf, i);
			total_ret += ret + i;
			buf = (char *)buf + ret + i;
			maxlen -= ret + i;
			pf->pending = 2 - i;
			pf->rec_pos = pos;
			pf->rec_offset = 0;
		}
		else {
			total_ret += ret;
			buf = (char *)buf + ret;
			maxlen -= ret;
			pf->rec_offset += ret;
		}
	}
#if defined(FILL_WITH_NULL) || defined(FILL_WITH_CTRLZ)
/*
 * If no bytes could be read, the file was already at the EOF position.
 * Check the file size from stat(). If it is greater than the stream
 * position (which is the number of bytes the file appears in stream mode),
 * we assume the caller insists of reading until what he thinks is the
 * whole file. We'll satisfy him by sending the missing number of bytes.
 * Yes, this is ugly, but what if applications just read and read until
 * all bytes given from stat() have been returned?
 */
	if (total_ret == 0  &&  pf->size > pf->stream_pos) {
		total_ret = pf->size - pf->stream_pos;
		if (total_ret > maxlen) total_ret = maxlen;
#ifdef FILL_WITH_NULL
		memset(buf, 0, total_ret);		/* return 0's */
#endif
#ifdef FILL_WITH_CTRLZ
		memset(buf, 0x1A, total_ret);	/* return CTRL/Z */
#endif
		DEBUG (3, ("stm_read: %d fill bytes returned\n",total_ret));
	}
#endif
/*
 * Record the 'stream position' (bytes from the beginning of the file
 * including the record terinator bytes).
 */
	pf->stream_pos += total_ret;
	return(total_ret);
}


/*
 * lseek
 *
 * The offset is given as stream bytes. Seeking forward means reading
 * the file to maintain the stream position. Seeking backward means
 * rewinding the file and start reading from the beginning.
 * Thus, the lseek may be very slow.
 */

long stm_lseek(int fd, long offset, int origin)
{
	struct fd_entry *pf;
	int pos;
	int ret;
	char *buf = NULL;
	int i;

	if (fd < 0) {
		errno = EBADF;
		return(-1);
	}
	pf = find_fd_entry(fd);
	if (pf == NULL || (!pf->record_mode)) return(lseek(fd, offset, origin));
/*
 * We can't seek backwards.
 * So we get the stream position of 'origin', add this to 'offset' and
 * use absolute seek from the beginning of the file.
 */
	if (offset < 0) {
		if (origin == SEEK_CUR) {
			offset += pf->stream_pos;
			origin = SEEK_SET;
		}
		else if (origin == SEEK_END) {
			pos = stm_lseek(fd, 0, SEEK_END); /* recursivly */
			offset += pos;
			origin = SEEK_SET;
		}
	}
/*
 * If the user just ask for the current position, why not simply return it...
 */
	else if (offset == 0) {
		if (origin == SEEK_CUR) {
			return(pf->stream_pos);
		}
	}
/*
 * If to seek before the beginning of the file, return error.
 */
	if (offset < 0) {
		errno = EINVAL;
		return(-1);
	}
/*
 * Seeking beyond the end of file is not permitted since we only support
 * reading files. convert to offset 0.
 */
	if (origin == SEEK_END) {
		if (offset > 0) offset = 0;
	}
/*
 * If now to seek from the beginning (SEEK_SET),
 * check if we are already positioned. Otherwise rewind the file.
 * If the user asks for the beginning, stop here.
 */
	if (origin == SEEK_SET) {
/*+++++		if (offset == pf->stream_pos) return(offset); /* ready!! */
		i = lseek(fd, 0, SEEK_SET);		/* same as rewind */
		pf->stream_pos = 0;
		pf->rec_pos = 0;
		pf->rec_offset = 0;
		pf->pending = 0;
		if (offset == 0) return(0);
	}
/*
 * Looking forward means reading the file. Allocate a buffer.
 */
	buf = (char *)malloc(SEEK_BUFFER);
	if (buf == 0) {
		DEBUG (0,("stm_lseek: can't allocate memory for buffer\n"));
		errno = ENOMEM;
		return(-1);
	}
/*
 * The following is possible now:
 *
 * - SEEK_CUR or SEEK_SET with positive (non 0) offset: read 'offset' bytes.
 * - SEEK_END with offset 0: read until end of file.
 */
	while (offset > 0 || origin == SEEK_END) {
		if (origin == SEEK_END) offset = SEEK_BUFFER;
		i = (offset > SEEK_BUFFER) ? SEEK_BUFFER : offset;
		ret = stm_read(fd, buf, i);
		if (ret == 0) break;	/* end of file */
		if (ret < 0) return(-1);	/* error */
		offset -= ret;
	}

	if (buf != NULL) free(buf);
	return(pf->stream_pos);
}


/* Check the VMS file type. Use the original C-RTL stat() routine here.
   If file is a VMS textfile, return size from stat() (this is convenient,
   since we will need the size later and need no further stat()).
   If not a VMS textfile, return 0.
*/

#include <fab.h>

static long is_textfile(char *fname)
{
/*	struct crtl_stat st; /**/
	struct stat st;
	int sts;

	sts = stat(fname,&st);
	if (sts != 0) return(sts);
	if (st.st_fab_rfm == FAB$C_VAR  &&  (st.st_fab_rat & FAB$M_CR))
		return(st.st_size);
	return(0);
}
