/** \ingroup rpmio
 * \file rpmio/gzdio.c
 * Support for ZLIB compression library.
 */

#include "system.h"

#if	defined(NOTYET) || defined(__LCLINT__)
#include <stdbool.h>
#else
typedef	enum { true = 1, false = 0 } bool;
#endif

#include "rpmio_internal.h"
#include <rpmmacro.h>
#include <rpmcb.h>

#ifdef	HAVE_ZLIB_H

/*@-noparams@*/
#include <zlib.h>
/*@=noparams@*/

#include "debug.h"

/*@access FD_t @*/

#define	GZDONLY(fd)	assert(fdGetIo(fd) == gzdio)

typedef struct cpio_state_s {
    uint32_t n;			/* byte progress in cpio header */
    uint32_t mode;			/* file attributes */
    uint32_t nlnk;
    uint32_t size;
} * cpio_state;

#define RSYNC_WIN 4096

typedef struct rsync_state_s {
    uint32_t n;			/* number of elements in the window */
    uint32_t sum;			/* current sum */
    unsigned char win[RSYNC_WIN];	/* window elements */
} * rsync_state;

typedef struct rpmGZFILE_s {
    gzFile gz;				/* gzFile is a pointer */
    struct rsync_state_s rs;
    struct cpio_state_s cs;
    uint32_t nb;			/* bytes pending for sync */
} * rpmGZFILE;				/* like FILE, to use with star */

/* Should gzflush be called only after RSYNC_WIN boundaries? */
/*@unchecked@*/
static int enable_rsync = 1;

/* =============================================================== */
/* from ../lib/cpio.h */
#define CPIO_NEWC_MAGIC "070701"
#define PHYS_HDR_SIZE 110

#define OFFSET_MODE (sizeof(CPIO_NEWC_MAGIC)-1 + 1*8)
#define OFFSET_NLNK (sizeof(CPIO_NEWC_MAGIC)-1 + 4*8)
#define OFFSET_SIZE (sizeof(CPIO_NEWC_MAGIC)-1 + 6*8)

static inline
int hex(char c)
{
    if (c >= '0' && c <= '9')
	return (int)(c - '0');
    else if (c >= 'a' && c <= 'f')
	return (int)(c - 'a') + 10;
    else if (c >= 'A' && c <= 'F')
	return (int)(c - 'A') + 10;
    return -1;
}

static inline
bool cpio_next(cpio_state s, unsigned char c)
	/*@modifies s @*/
{
    if (s->n >= sizeof(CPIO_NEWC_MAGIC)-1) {
	int d = hex(c);
	if (d < 0) {
	    s->n = 0;
	    return false;
	}
	if (0){} /* indent */
	else if (s->n >= OFFSET_MODE && s->n < OFFSET_MODE+8) {
	    if (s->n == OFFSET_MODE)
		s->mode = 0;
	    else
		s->mode <<= 4;
	    s->mode |= d;
	}
	else if (s->n >= OFFSET_NLNK && s->n < OFFSET_NLNK+8) {
	    if (s->n == OFFSET_NLNK)
		s->nlnk = 0;
	    else
		s->nlnk <<= 4;
	    s->nlnk |= d;
	}
	else if (s->n >= OFFSET_SIZE && s->n < OFFSET_SIZE+8) {
	    if (s->n == OFFSET_SIZE)
		s->size = 0;
	    else
		s->size <<= 4;
	    s->size |= d;
	}
	s->n++;
	if (s->n >= PHYS_HDR_SIZE) {
	    s->n = 0;
	    if (!S_ISREG(s->mode) || s->nlnk != 1)
		/* no file data */
		s->size = 0;
	    return true;
	}
    }
    else if (CPIO_NEWC_MAGIC[s->n] == c) {
	s->n++;
    }
    else {
	s->n = 0;
    }
    return false;
}

/* =============================================================== */
static inline
bool rsync_next(rsync_state s, unsigned char c)
	/*@modifies s @*/
{
    uint32_t i;

    if (s->n < RSYNC_WIN) {		/* not enough elements */
	s->sum += (uint32_t)c;	/* update the sum */
	s->win[s->n++] = c;		/* remember the element */
	return false;			/* no match */
    }
    i = s->n++ % RSYNC_WIN;		/* wrap up */
    s->sum -= (uint32_t)s->win[i];	/* move the window on */
    s->sum += (uint32_t)c;
    s->win[i] = c;
    if (s->sum % RSYNC_WIN == 0) {	/* match */
	s->n = 0;			/* reset */
	s->sum = 0;
	return true;
    }
    return false;
}

#define CHUNK 4096

static inline
bool sync_hint(rpmGZFILE rpmgz, unsigned char c)
	/*@modifies rpmgz @*/
{
    bool cpio_hint;
    bool rsync_hint;

    rpmgz->nb++;
    cpio_hint = cpio_next(&rpmgz->cs, c);
    if (cpio_hint) {
	/* cpio header/data boundary */
	rpmgz->rs.n = rpmgz->rs.sum = 0;
	if (rpmgz->nb >= 2*CHUNK)
	    /* better sync here */
	    goto cpio_sync;
	if (rpmgz->cs.size < CHUNK)
	    /* file is too small */
	    return false;
	if (rpmgz->nb < CHUNK/2)
	    /* not enough pending bytes */
	    return false;
    cpio_sync:
	rpmgz->nb = 0;
	return true;
    }
    rsync_hint = rsync_next(&rpmgz->rs, c);
    if (rsync_hint) {
	/* rolling checksum match */
	assert(rpmgz->nb >= RSYNC_WIN);
	rpmgz->nb = 0;
	return true;
    }
    return false;
}

static ssize_t
rsyncable_gzwrite(rpmGZFILE rpmgz, const unsigned char *const buf, const size_t len)
	/*@globals fileSystem @*/
	/*@modifies rpmgz, fileSystem @*/
{
    ssize_t rc;
    size_t n;
    ssize_t n_written = 0;
    const unsigned char *begin = buf;
    size_t i;

    for (i = 0; i < len; i++) {
	if (!sync_hint(rpmgz, buf[i]))
	    continue;
	n = i + 1 - (begin - buf);
	rc = gzwrite(rpmgz->gz, begin, (unsigned)n);
	if (rc < 0)
	    return (n_written ? n_written : rc);
	n_written += rc;
	if (rc < (ssize_t)n)
	    return n_written;
	begin += n;
	rc = gzflush(rpmgz->gz, Z_SYNC_FLUSH);
	if (rc < 0)
	    return (n_written ? n_written : rc);
    }
    if (begin < buf + len) {
	n = len - (begin - buf);
	rc = gzwrite(rpmgz->gz, begin, (unsigned)n);
	if (rc < 0)
	    return (n_written ? n_written : rc);
	n_written += rc;
    }
    return n_written;
}

/* =============================================================== */
/*@-moduncon@*/

static inline /*@dependent@*/ /*@null@*/ void * gzdFileno(FD_t fd)
	/*@*/
{
    void * rc = NULL;
    int i;

    FDSANE(fd);
    for (i = fd->nfps; i >= 0; i--) {
	FDSTACK_t * fps = &fd->fps[i];
	if (fps->io != gzdio)
	    continue;
	rc = fps->fp;
	break;
    }

    return rc;
}

static /*@null@*/
FD_t gzdOpen(const char * path, const char * fmode)
	/*@globals fileSystem, internalState @*/
	/*@modifies fileSystem, internalState @*/
{
    FD_t fd;
    rpmGZFILE rpmgz;
    mode_t mode = (fmode && fmode[0] == 'w' ? O_WRONLY : O_RDONLY);

    rpmgz = xcalloc(1, sizeof(*rpmgz));
    rpmgz->gz = gzopen(path, fmode);
    if (rpmgz->gz == NULL) {
	rpmgz = _free(rpmgz);
	return NULL;
    }
    fd = fdNew("open (gzdOpen)");
    fdPop(fd); fdPush(fd, gzdio, rpmgz, -1);
    fdSetOpen(fd, path, -1, mode);

DBGIO(fd, (stderr, "==>\tgzdOpen(\"%s\", \"%s\") fd %p %s\n", path, fmode, (fd ? fd : NULL), fdbg(fd)));
    return fdLink(fd, "gzdOpen");
}

static /*@null@*/ FD_t gzdFdopen(void * cookie, const char *fmode)
	/*@globals fileSystem, internalState @*/
	/*@modifies fileSystem, internalState @*/
{
    FD_t fd = c2f(cookie);
    int fdno;
    rpmGZFILE rpmgz;

    if (fmode == NULL) return NULL;
    fdno = fdFileno(fd);
    fdSetFdno(fd, -1);		/* XXX skip the fdio close */
    if (fdno < 0) return NULL;
    rpmgz = xcalloc(1, sizeof(*rpmgz));
    rpmgz->gz = gzdopen(fdno, fmode);
    if (rpmgz->gz == NULL) {
	rpmgz = _free(rpmgz);
	return NULL;
    }

    fdPush(fd, gzdio, rpmgz, fdno);		/* Push gzdio onto stack */

    return fdLink(fd, "gzdFdopen");
}

static int gzdFlush(void * cookie)
	/*@globals fileSystem @*/
	/*@modifies fileSystem @*/
{
    FD_t fd = c2f(cookie);
    rpmGZFILE rpmgz;
    rpmgz = gzdFileno(fd);
    if (rpmgz == NULL) return -2;
    return gzflush(rpmgz->gz, Z_SYNC_FLUSH);	/* XXX W2DO? */
}

/* =============================================================== */
static ssize_t gzdRead(void * cookie, /*@out@*/ char * buf, size_t count)
	/*@globals fileSystem, internalState @*/
	/*@modifies buf, fileSystem, internalState @*/
{
    FD_t fd = c2f(cookie);
    rpmGZFILE rpmgz;
    ssize_t rc;

    if (fd == NULL || fd->bytesRemain == 0) return 0;	/* XXX simulate EOF */

    rpmgz = gzdFileno(fd);
    if (rpmgz == NULL) return -2;	/* XXX can't happen */

    fdstat_enter(fd, FDSTAT_READ);
    rc = gzread(rpmgz->gz, buf, (unsigned)count);
DBGIO(fd, (stderr, "==>\tgzdRead(%p,%p,%u) rc %lx %s\n", cookie, buf, (unsigned)count, (unsigned long)rc, fdbg(fd)));
    if (rc < 0) {
	int zerror = 0;
	fd->errcookie = gzerror(rpmgz->gz, &zerror);
	if (zerror == Z_ERRNO) {
	    fd->syserrno = errno;
	    fd->errcookie = strerror(fd->syserrno);
	}
    } else {
	fdstat_exit(fd, FDSTAT_READ, (rc > 0 ? rc : 0));
	if (fd->ndigests && rc > 0) fdUpdateDigests(fd, (void *)buf, rc);
    }
    return rc;
}

static ssize_t gzdWrite(void * cookie, const char * buf, size_t count)
	/*@globals fileSystem, internalState @*/
	/*@modifies fileSystem, internalState @*/
{
    FD_t fd = c2f(cookie);
    rpmGZFILE rpmgz;
    ssize_t rc;

    if (fd == NULL || fd->bytesRemain == 0) return 0;	/* XXX simulate EOF */

    if (fd->ndigests && count > 0) fdUpdateDigests(fd, (void *)buf, count);

    rpmgz = gzdFileno(fd);
    if (rpmgz == NULL) return -2;	/* XXX can't happen */

    fdstat_enter(fd, FDSTAT_WRITE);
    if (enable_rsync)
	rc = rsyncable_gzwrite(rpmgz, (void *)buf, (unsigned)count);
    else
	rc = gzwrite(rpmgz->gz, (void *)buf, (unsigned)count);
DBGIO(fd, (stderr, "==>\tgzdWrite(%p,%p,%u) rc %lx %s\n", cookie, buf, (unsigned)count, (unsigned long)rc, fdbg(fd)));
    if (rc < (ssize_t)count) {
	int zerror = 0;
	fd->errcookie = gzerror(rpmgz->gz, &zerror);
	if (zerror == Z_ERRNO) {
	    fd->syserrno = errno;
	    fd->errcookie = strerror(fd->syserrno);
	}
    }
    if (rc > 0)
	fdstat_exit(fd, FDSTAT_WRITE, rc);
    return rc;
}

/* XXX zlib-1.0.4 has not */
#define	HAVE_GZSEEK	/* XXX autoFu doesn't set this anymore. */
static inline int gzdSeek(void * cookie, _libio_pos_t pos, int whence)
	/*@globals fileSystem, internalState @*/
	/*@modifies fileSystem, internalState @*/
{
    int rc;
#if defined(HAVE_GZSEEK)
#ifdef USE_COOKIE_SEEK_POINTER
    _IO_off64_t p = *pos;
#else
    off_t p = pos;
#endif
    FD_t fd = c2f(cookie);
    rpmGZFILE rpmgz;

    if (fd == NULL) return -2;
    assert(fd->bytesRemain == -1);	/* XXX FIXME */

    rpmgz = gzdFileno(fd);
    if (rpmgz == NULL) return -2;	/* XXX can't happen */

    fdstat_enter(fd, FDSTAT_SEEK);
    rc = gzseek(rpmgz->gz, (long)p, whence);
DBGIO(fd, (stderr, "==>\tgzdSeek(%p,%ld,%d) rc %lx %s\n", cookie, (long)p, whence, (unsigned long)rc, fdbg(fd)));
    if (rc < 0) {
	int zerror = 0;
	fd->errcookie = gzerror(rpmgz->gz, &zerror);
	if (zerror == Z_ERRNO) {
	    fd->syserrno = errno;
	    fd->errcookie = strerror(fd->syserrno);
	}
    }
    if (rc > 0)
	fdstat_exit(fd, FDSTAT_SEEK, rc);
#else
    rc = -2;
#endif
    return rc;
}

static int gzdClose( /*@only@*/ void * cookie)
	/*@globals fileSystem, internalState @*/
	/*@modifies fileSystem, internalState @*/
{
    FD_t fd = c2f(cookie);
    rpmGZFILE rpmgz;
    int rc;

    rpmgz = gzdFileno(fd);
    if (rpmgz == NULL) return -2;	/* XXX can't happen */

    fdstat_enter(fd, FDSTAT_CLOSE);
    /*@-dependenttrans@*/
    rc = gzclose(rpmgz->gz);
    /*@=dependenttrans@*/
    rpmgz->gz = NULL;
/*@-dependenttrans@*/
    rpmgz = _free(rpmgz);
/*@=dependenttrans@*/

    /* XXX TODO: preserve fd if errors */

    if (fd) {
DBGIO(fd, (stderr, "==>\tgzdClose(%p) zerror %d %s\n", cookie, rc, fdbg(fd)));
	if (rc < 0) {
	    fd->errcookie = "gzclose error";
	    if (rc == Z_ERRNO) {
		fd->syserrno = errno;
		fd->errcookie = strerror(fd->syserrno);
	    }
	} else if (rc >= 0) {
	    fdstat_exit(fd, FDSTAT_CLOSE, rc);
	}
    }

DBGIO(fd, (stderr, "==>\tgzdClose(%p) rc %lx %s\n", cookie, (unsigned long)rc, fdbg(fd)));

    if (_rpmio_debug || rpmIsDebug()) fdstat_print(fd, "GZDIO", stderr);
    if (rc == 0)
	fd = fdFree(fd, "open (gzdClose)");
    return rc;
}

/*@-type@*/ /* LCL: function typedefs */
static struct FDIO_s gzdio_s = {
  gzdRead, gzdWrite, gzdSeek, gzdClose,	gzdOpen, gzdFdopen, gzdFlush,
};
/*@=type@*/

FDIO_t gzdio = /*@-compmempass@*/ &gzdio_s /*@=compmempass@*/ ;

/*@=moduncon@*/
#endif	/* HAVE_ZLIB_H */
