/* Copyright (C) 1994, 2000 artofcode LLC.  All rights reserved.
  
  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.,
  59 Temple Place, Suite 330, Boston, MA, 02111-1307.

*/

/*$Id: sfxfd.c,v 1.6.4.1.2.1 2003/01/17 00:49:05 giles Exp $ */
/* File stream implementation using direct OS calls */
/******
 ****** NOTE: THIS FILE MAY NOT COMPILE ON NON-UNIX PLATFORMS, AND MAY
 ****** REQUIRE EDITING ON SOME UNIX PLATFORMS.
 ******/
#include "stdio_.h"		/* includes std.h */
#include "errno_.h"
#include "memory_.h"
#include "unistd_.h"            /* for read, write, fsync, lseek */

#include "gdebug.h"
#include "gpcheck.h"
#include "stream.h"
#include "strimpl.h"

/*
 * This is an alternate implementation of file streams.  It still uses
 * FILE * in the interface, but it uses direct OS calls for I/O.
 * It also includes workarounds for the nasty habit of System V Unix
 * of breaking out of read and write operations with EINTR, EAGAIN,
 * and/or EWOULDBLOCK "errors".
 *
 * The interface should be identical to that of sfxstdio.c.  However, in
 * order to allow both implementations to exist in the same executable, we
 * optionally use different names for sread_file, swrite_file, and
 * sappend_file, and omit sread_subfile (the public procedures).
 * See sfxboth.c.
 */
#ifdef KEEP_FILENO_API
/* Provide prototypes to avoid compiler warnings. */
void
    sread_fileno(P4(stream *, FILE *, byte *, uint)),
    swrite_fileno(P4(stream *, FILE *, byte *, uint)),
    sappend_fileno(P4(stream *, FILE *, byte *, uint));
#else
#  define sread_fileno sread_file
#  define swrite_fileno swrite_file
#  define sappend_fileno sappend_file
#endif

/* Forward references for file stream procedures */
private int
    s_fileno_available(P2(stream *, long *)),
    s_fileno_read_seek(P2(stream *, long)),
    s_fileno_read_close(P1(stream *)),
    s_fileno_read_process(P4(stream_state *, stream_cursor_read *,
			     stream_cursor_write *, bool));
private int
    s_fileno_write_seek(P2(stream *, long)),
    s_fileno_write_flush(P1(stream *)),
    s_fileno_write_close(P1(stream *)),
    s_fileno_write_process(P4(stream_state *, stream_cursor_read *,
			      stream_cursor_write *, bool));
private int
    s_fileno_switch(P2(stream *, bool));

/* Get the file descriptor number of a stream. */
inline private int
sfileno(const stream *s)
{
    return fileno(s->file);
}

/* Get the current position of a file descriptor. */
inline private long
ltell(int fd)
{
    return lseek(fd, 0L, SEEK_CUR);
}

/* Define the System V interrupts that require retrying a call. */
private bool
errno_is_retry(int errn)
{
    switch (errn) {
#ifdef EINTR
    case EINTR: return true;
#endif
#if defined(EAGAIN) && (!defined(EINTR) || EAGAIN != EINTR)
    case EAGAIN: return true;
#endif
#if defined(EWOULDBLOCK) && (!defined(EINTR) || EWOULDBLOCK != EINTR) && (!defined(EAGAIN) || EWOULDBLOCK != EAGAIN)
    case EWOULDBLOCK: return true;
#endif
    default: return false;
    }
}

/* ------ File reading ------ */

/* Initialize a stream for reading an OS file. */
void
sread_fileno(register stream * s, FILE * file, byte * buf, uint len)
{
    static const stream_procs p = {
	s_fileno_available, s_fileno_read_seek, s_std_read_reset,
	s_std_read_flush, s_fileno_read_close, s_fileno_read_process,
	s_fileno_switch
    };
    /*
     * There is no really portable way to test seekability,
     * but this should work on most systems.
     */
    int fd = fileno(file);
    long curpos = ltell(fd);
    bool seekable = (curpos != -1L && lseek(fd, curpos, SEEK_SET) != -1L);

    s_std_init(s, buf, len, &p,
	       (seekable ? s_mode_read + s_mode_seek : s_mode_read));
    if_debug2('s', "[s]read file=0x%lx, fd=%d\n", (ulong) file,
	      fileno(file));
    s->file = file;
    s->file_modes = s->modes;
    s->file_offset = 0;
    s->file_limit = max_long;
}

/* Confine reading to a subfile.  This is primarily for reusable streams. */
/*
 * We omit this procedure if we are also include sfxstdio.c, which has an
 * identical definition.
 */
#ifndef KEEP_FILENO_API
int
sread_subfile(stream *s, long start, long length)
{
    if (s->file == 0 || s->modes != s_mode_read + s_mode_seek ||
	s->file_offset != 0 || s->file_limit != max_long ||
	((s->position < start || s->position > start + length) &&
	 sseek(s, start) < 0)
	)
	return ERRC;
    s->position -= start;
    s->file_offset = start;
    s->file_limit = length;
    return 0;
}
#endif

/* Procedures for reading from a file */
private int
s_fileno_available(register stream * s, long *pl)
{
    long max_avail = s->file_limit - stell(s);
    long buf_avail = sbufavailable(s);
    int fd = sfileno(s);

    *pl = min(max_avail, buf_avail);
    if (sseekable(s)) {
	long pos, end;

	pos = ltell(fd);
	if (pos < 0)
	    return ERRC;
	end = lseek(fd, 0L, SEEK_END);
	if (lseek(fd, pos, SEEK_SET) < 0 || end < 0)
	    return ERRC;
	buf_avail += end - pos;
	*pl = min(max_avail, buf_avail);
	if (*pl == 0)
	    *pl = -1;		/* EOF */
    } else {
	if (*pl == 0)
	    *pl = -1;		/* EOF */
    }
    return 0;
}
private int
s_fileno_read_seek(register stream * s, long pos)
{
    uint end = s->srlimit - s->cbuf + 1;
    long offset = pos - s->position;

    if (offset >= 0 && offset <= end) {  /* Staying within the same buffer */
	s->srptr = s->cbuf + offset - 1;
	return 0;
    }
    if (pos < 0 || pos > s->file_limit ||
	lseek(sfileno(s), s->file_offset + pos, SEEK_SET) < 0
	)
	return ERRC;
    s->srptr = s->srlimit = s->cbuf - 1;
    s->end_status = 0;
    s->position = pos;
    return 0;
}
private int
s_fileno_read_close(stream * s)
{
    FILE *file = s->file;

    if (file != 0) {
	s->file = 0;
	return (fclose(file) ? ERRC : 0);
    }
    return 0;
}

/* Process a buffer for a file reading stream. */
/* This is the first stream in the pipeline, so pr is irrelevant. */
private int
s_fileno_read_process(stream_state * st, stream_cursor_read * ignore_pr,
		      stream_cursor_write * pw, bool last)
{
    stream *s = (stream *)st;	/* no separate state */
    int fd = sfileno(s);
    uint max_count;
    int nread, status;

again:
    max_count = pw->limit - pw->ptr;
    status = 1;
    if (s->file_limit < max_long) {
	long limit_count = s->file_offset + s->file_limit - ltell(fd);

	if (max_count > limit_count)
	    max_count = limit_count, status = EOFC;
    }
    /*
     * In the Mac MetroWerks compiler, the prototype for read incorrectly
     * declares the second argument of read as char * rather than void *.
     * Work around this here.
     */
    nread = read(fd, (void *)(pw->ptr + 1), max_count);
    if (nread > 0)
	pw->ptr += nread;
    else if (nread == 0)
	status = EOFC;
    else if (errno_is_retry(errno))	/* Handle System V interrupts */
	goto again;
    else
	status = ERRC;
    process_interrupts();
    return status;
}

/* ------ File writing ------ */

/* Initialize a stream for writing an OS file. */
void
swrite_fileno(register stream * s, FILE * file, byte * buf, uint len)
{
    static const stream_procs p = {
	s_std_noavailable, s_fileno_write_seek, s_std_write_reset,
	s_fileno_write_flush, s_fileno_write_close, s_fileno_write_process,
	s_fileno_switch
    };

    s_std_init(s, buf, len, &p,
	       (file == stdout ? s_mode_write : s_mode_write + s_mode_seek));
    if_debug2('s', "[s]write file=0x%lx, fd=%d\n", (ulong) file,
	      fileno(file));
    s->file = file;
    s->file_modes = s->modes;
    s->file_offset = 0;		/* in case we switch to reading later */
    s->file_limit = max_long;	/* ibid. */
}
/* Initialize for appending to an OS file. */
void
sappend_fileno(register stream * s, FILE * file, byte * buf, uint len)
{
    swrite_fileno(s, file, buf, len);
    s->modes = s_mode_write + s_mode_append;	/* no seek */
    s->file_modes = s->modes;
    s->position = lseek(fileno(file), 0L, SEEK_END);
}
/* Procedures for writing on a file */
private int
s_fileno_write_seek(stream * s, long pos)
{
    /* We must flush the buffer to reposition. */
    int code = sflush(s);

    if (code < 0)
	return code;
    if (lseek(sfileno(s), pos, SEEK_SET) < 0)
	return ERRC;
    s->position = pos;
    return 0;
}
private int
s_fileno_write_flush(register stream * s)
{
    int result = s_process_write_buf(s, false);

    discard(fsync(sfileno(s)));
    return result;
}
private int
s_fileno_write_close(register stream * s)
{
    s_process_write_buf(s, true);
    return s_fileno_read_close(s);
}

/* Process a buffer for a file writing stream. */
/* This is the last stream in the pipeline, so pw is irrelevant. */
private int
s_fileno_write_process(stream_state * st, stream_cursor_read * pr,
		       stream_cursor_write * ignore_pw, bool last)
{
    int nwrite, status;
    uint count;

again:
    count = pr->limit - pr->ptr;
    /* Some versions of the DEC C library on AXP architectures */
    /* give an error on write if the count is zero! */
    if (count == 0) {
	process_interrupts();
	return 0;
    }
    /* See above regarding the Mac MetroWorks compiler. */
    nwrite = write(sfileno((stream *)st), (const void *)(pr->ptr + 1), count);
    if (nwrite >= 0) {
	pr->ptr += nwrite;
	status = 0;
    } else if (errno_is_retry(errno))	/* Handle System V interrupts */
	goto again;
    else
	status = ERRC;
    process_interrupts();
    return status;
}

/* ------ File switching ------ */

/* Switch a file stream to reading or writing. */
private int
s_fileno_switch(stream * s, bool writing)
{
    uint modes = s->file_modes;
    int fd = sfileno(s);
    long pos;

    if (writing) {
	if (!(s->file_modes & s_mode_write))
	    return ERRC;
	pos = stell(s);
	if_debug2('s', "[s]switch 0x%lx to write at %ld\n",
		  (ulong) s, pos);
	lseek(fd, pos, SEEK_SET);	/* pacify OS */
	if (modes & s_mode_append) {
	    sappend_file(s, s->file, s->cbuf, s->cbsize);  /* sets position */
	} else {
	    swrite_file(s, s->file, s->cbuf, s->cbsize);
	    s->position = pos;
	}
	s->modes = modes;
    } else {
	if (!(s->file_modes & s_mode_read))
	    return ERRC;
	pos = stell(s);
	if_debug2('s', "[s]switch 0x%lx to read at %ld\n",
		  (ulong) s, pos);
	if (sflush(s) < 0)
	    return ERRC;
	lseek(fd, 0L, SEEK_CUR);	/* pacify OS */
	sread_file(s, s->file, s->cbuf, s->cbsize);
	s->modes |= modes & s_mode_append;	/* don't lose append info */
	s->position = pos;
    }
    s->file_modes = modes;
    return 0;
}
