/*
 * The contents of this file are subject to the AOLserver Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://aolserver.lcs.mit.edu/.
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) 2001-2003 Vlad Seryakov
 * All rights reserved.
 *
 * Alternatively, the contents of this file may be used under the terms
 * of the GNU General Public License (the "GPL"), in which case the
 * provisions of GPL are applicable instead of those above.  If you wish
 * to allow use of your version of this file only under the terms of the
 * GPL and not to allow others to use your version of this file under the
 * License, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the GPL.
 * If you do not delete the provisions above, a recipient may use your
 * version of this file under either the License or the GPL.
 */

/*
 * nszlib.c -- Zlib API module
 *
 *  ns_zlib usage:
 *
 *    ns_zlib compress data
 *      Returns compressed string
 *
 *    ns_zlib uncompress data
 *       Uncompresses previously compressed string
 *
 *    ns_zlib gzip data
 *      Returns compressed string in gzip format, string can be saved in
 *      a file with extension .gz and gzip will be able to uncompress it
 *
 *     ns_zlib gzipfile file
 *      Compresses the specified file, creating a file with the
 *      same name but a .gz suffix appened
 *
 *    ns_zlib gunzip file
 *       Uncompresses gzip file and returns text
 *
 *
 * Authors
 *
 *     Vlad Seryakov vlad@crystalballinc.com
 */

#include "nszlib.h"

#define VERSION "4.5"

static char header[] = {
    037, 0213,  /* GZIP magic number. */
    010,        /* Z_DEFLATED */
    0,          /* flags */
    0,0,0,0,    /* timestamp */
    0,          /* xflags */
    03};        /* Unix OS_CODE */

static Ns_TclTraceProc ZlibTrace;
static Tcl_ObjCmdProc ZlibObjCmd;
static Ns_GzipProc ZlibGzip;


/*
 *----------------------------------------------------------------------
 *
 * Nszlib_Init--
 *
 *      Tcl load-command entry point.
 *
 * Results:
 *      TCL_OK.
 *
 * Side effects:
 *	Adds the ns_zlib command.
 *
 *----------------------------------------------------------------------
 */

int
Nszlib_Init(Tcl_Interp *interp)
{
    Tcl_CreateObjCommand(interp, "ns_zlib",ZlibObjCmd ,NULL, NULL);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * NsZlibModInit--
 *
 *      AOLserver module entry point.
 *
 * Results:
 *      NS_OK.
 *
 * Side effects:
 *	Installs ZlibGzip as the Ns_Gzip proc and registers
 *	a trace to create ns_zlib command in all new interps.
 *
 *----------------------------------------------------------------------
 */

int
NsZlibModInit(char *server, char *module)
{
    Ns_SetGzipProc(ZlibGzip);
    Ns_TclRegisterTrace(server, ZlibTrace, NULL, NS_TCL_TRACE_CREATE);
    return NS_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * Ns_ZlibCompress --
 *
 *      Compress a string.
 *
 * Results:
 *	Pointer to ns_malloc's string of compressed data or NULL
 *	on error.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

unsigned char *
Ns_ZlibCompress(unsigned char *inbuf,unsigned long inlen,unsigned long *outlen)
{
    int rc;
    unsigned long crc;
    unsigned char *outbuf;

    *outlen = inlen*1.1+20;
    outbuf = ns_malloc(*outlen);
    *outlen = (*outlen)-8;
    rc = compress2(outbuf,outlen,inbuf,inlen,3);
    if(rc != Z_OK) {
	Ns_Log(Error,"Ns_ZlibCompress: error %d",rc);
	ns_free(outbuf);
	return 0;
    }
    crc = crc32(crc32(0,Z_NULL,0),inbuf,inlen);
    crc = htonl(crc);
    inlen = htonl(inlen);
    memcpy(outbuf+(*outlen),&crc,4);
    memcpy(outbuf+(*outlen)+4,&inlen,4);
    (*outlen) += 8;
    return outbuf;
}


/*
 *----------------------------------------------------------------------
 *
 * Ns_ZlibUncompress --
 *
 *      Uncompress a string.
 *
 * Results:
 *      Pointer to ns_malloc'ed string of uncompressed data or NULL
 *	on error.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

unsigned char *
Ns_ZlibUncompress(unsigned char *inbuf,unsigned long inlen,
		  unsigned long *outlen)
{
    int rc;
    unsigned long crc;
    unsigned char *outbuf;

    memcpy(outlen,&inbuf[inlen-4],4);
    *outlen = ntohl(*outlen);
    outbuf = ns_malloc((*outlen)+1);
    rc = uncompress(outbuf,outlen,inbuf,inlen-8);
    if(rc != Z_OK) {
	Ns_Log(Error,"Ns_ZlibUncompress: error %d",rc);
	ns_free(outbuf);
	return 0;
    }
    memcpy(&crc,&inbuf[inlen-8],4);
    crc = ntohl(crc);
    if(crc != crc32(crc32(0,Z_NULL,0),outbuf,*outlen)) {
	Ns_Log(Error,"Ns_ZlibUncompress: crc mismatch");
	ns_free(outbuf);
	return 0;
    }
    return outbuf;
}


/*
 *----------------------------------------------------------------------
 *
 * ZlibGzip --
 *
 *      Compress procedure for Ns_Gzip.
 *
 * Results:
 *      NS_OK if compression worked, NS_ERROR otherwise.
 *
 * Side effects:
 *      Will write compressed content to given Tcl_DString.
 *
 *----------------------------------------------------------------------
 */

static int
ZlibGzip(char *buf, int len, int level, Tcl_DString *dsPtr)
{
    Bytef *ubuf = (Bytef *) buf;
    uLong ulen = (uLong) len;
#define FOOTER_SIZE	8
    unsigned char *fp;
    uLongf glen;
    Bytef *gbuf;
    uLong crc;
    int skip;

    /*
     * Size the dstring to hold the header, footer, and max compressed output.
     */

    glen = (uLongf) compressBound(len) + sizeof(header) + FOOTER_SIZE;
    Tcl_DStringSetLength(dsPtr, (int) glen);

    /*
     * Compress output to the dstring starting 2-bytes from the end of
     * the where the header will be written and then write the header
     * at the start of the dstring.
     */

    gbuf = (Bytef *) dsPtr->string;
    skip = sizeof(header) - 2;
    glen -= skip;
    if (compress2(gbuf + skip, &glen, ubuf, ulen, level) != Z_OK) {
		  return NS_ERROR;
    }
    memcpy(gbuf, header, sizeof(header));

    /*
     * Adjust the size of the dstring to the actual size of compressed data,
     * header, and footer, less the 2-bytes overwritten by the header
     * and the 2-bytes to be overwritten by the footer.
     */

    glen = glen + sizeof(header) - 6;
    Tcl_DStringSetLength(dsPtr, (int) glen + FOOTER_SIZE);

    /*
     * Calculate the CRC and append it and length as the footer.
     */

    crc = crc32(0, Z_NULL, 0);
    crc = crc32(crc, ubuf, ulen);
    fp = (unsigned char *) dsPtr->string + glen;
    *fp++ = (crc & 0x000000ff);
    *fp++ = (crc & 0x0000ff00) >>  8;
    *fp++ = (crc & 0x00ff0000) >> 16;
    *fp++ = (crc & 0xff000000) >> 24;
    *fp++ = (len & 0x000000ff);
    *fp++ = (len & 0x0000ff00) >>  8;
    *fp++ = (len & 0x00ff0000) >> 16;
    *fp++ = (len & 0xff000000) >> 24;
    return NS_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * ZlibTrace --
 *
 *      AOLserver Tcl trace to add ns_zlib command.
 *
 * Results:
 *      NS_OK.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static int
ZlibTrace(Tcl_Interp *interp, void *ignored)
{
    Nszlib_Init(interp);
    return NS_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * ZlibObjCmd --
 *
 *      Implements the ns_zlib command.
 *
 * Results:
 *      Standard Tcl result.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

static
int ZlibObjCmd(ClientData ignored, Tcl_Interp *interp, int objc,
		Tcl_Obj * CONST objv[])
{
    Tcl_DString ds;
    int fd, rc, nread;
    Tcl_Obj *obj;
    char *ifile, *ofile, buf[32768];
    gzFile gin, gout;
    unsigned char *inbuf,*outbuf;
    unsigned long inlen,outlen;
    static CONST char *opts[] = {
	"compress", "uncompress", "gzip", "gunzip", "gzipfile",
	NULL
    };
    enum {
	ZCompIdx, ZUnCompIdx, ZGzipIdx, ZGunzipIdx, ZGzipFileIdx
    } opt;

    if (objc < 2) {
		  Tcl_WrongNumArgs(interp, 1, objv, "option ?arg?");
		  return TCL_ERROR;
    }
    if (Tcl_GetIndexFromObj(interp, objv[1], opts, "option", 0,
		                      (int *) &opt) != TCL_OK) {
		  return TCL_ERROR;
    }

    switch (opt) {
    case ZCompIdx:
	inbuf = Tcl_GetByteArrayFromObj(objv[2],(int*)&inlen);
	if(!(outbuf = Ns_ZlibCompress(inbuf,inlen,&outlen))) {
	    Tcl_AppendResult(interp,"nszlib: compress failed",0);
	     return TCL_ERROR;
	}
	Tcl_SetObjResult(interp,Tcl_NewByteArrayObj(outbuf,(int)outlen));
	ns_free(outbuf);
	break;

    case ZUnCompIdx:
	inbuf = Tcl_GetByteArrayFromObj(objv[2],(int*)&inlen);
	if(!(outbuf = Ns_ZlibUncompress(inbuf,inlen,&outlen))) {
	    Tcl_AppendResult(interp,"nszlib: uncompress failed",0);
	    return TCL_ERROR;
	}
	Tcl_SetObjResult(interp,Tcl_NewStringObj((char *) outbuf,(int)outlen));
	ns_free(outbuf);
	break;

    case ZGzipIdx:
	inbuf = Tcl_GetByteArrayFromObj(objv[2],(int*)&inlen);
	obj = NULL;
	Tcl_DStringInit(&ds);
	if (ZlibGzip((char *) inbuf, inlen, 3, &ds) == NS_OK) {
	    obj = Tcl_NewByteArrayObj((unsigned char *) ds.string, ds.length);
	}
	Tcl_DStringFree(&ds);
	if (obj == NULL) {
	    Tcl_AppendResult(interp, "gzip failed", NULL);
	    return TCL_ERROR;
	}
	Tcl_SetObjResult(interp, obj);
	break;

     case ZGunzipIdx:
	gin  = gzopen(Tcl_GetString(objv[2]), "rb");
	if(!gin) {
	    Tcl_AppendResult(interp,"nszlib: gunzip: cannot open ",
			     Tcl_GetString(objv[2]),0);
	    return TCL_ERROR;
	}
	obj = Tcl_NewStringObj(0,0);
	for(;;) {
	    nread = gzread(gin, buf, sizeof(buf));
	    if (nread == 0) {
		break;
	    } else if (nread < 0) {
		Tcl_AppendResult(interp,"nszlib: gunzip: read error ",
				 gzerror(gin,&rc),0);
		Tcl_DecrRefCount(obj);
		gzclose(gin);
		return TCL_ERROR;
	    }
	    Tcl_AppendToObj(obj, buf, nread);
	}
	Tcl_SetObjResult(interp,obj);
	gzclose(gin);
	break;

    case ZGzipFileIdx:
	ifile = Tcl_GetString(objv[2]);
	fd = open(ifile, O_RDONLY|O_BINARY);
	if (fd < 0) {
	    Tcl_AppendResult(interp,"nszlib: gzipfile: cannot open ",
			     ifile, 0);
	    return TCL_ERROR;
	}
	obj = Tcl_NewStringObj(Tcl_GetString(objv[2]),-1);
	Tcl_AppendToObj(obj,".gz",3);
	ofile = Tcl_GetString(obj);
	if(!(gout = gzopen(ofile,"wb"))) {
	    Tcl_AppendResult(interp,"nszlib: gzipfile: cannot create ",
		ofile, 0);
	    Tcl_DecrRefCount(obj);
	    return TCL_ERROR;
	}
	for(;;) {
	    nread = read(fd, buf, sizeof(buf));
	    if (nread == 0) {
		break;
	    }
	    if (nread < 0) {
		Tcl_AppendResult(interp,"nszlib: gzipfile: read error ",
				 strerror(errno),0);
		goto err;
	    }
	    if(gzwrite(gout, buf, (unsigned) nread) != nread) {
		Tcl_AppendResult(interp,"nszlib: gunzip: write error ",
				 gzerror(gout,&rc),0);
err:
		close(fd);
		gzclose(gout);
		unlink(ofile);
		Tcl_DecrRefCount(obj);
		return TCL_ERROR;
	    }
	}
	close(fd);
	gzclose(gout);
	unlink(ifile);
	Tcl_SetObjResult(interp, obj);
	break;
    }
    return TCL_OK;
}
