Blob Blame History Raw
/*
 * $LynxId: HTFormat.c,v 1.90 2018/05/11 22:18:24 tom Exp $
 *
 *		Manage different file formats			HTFormat.c
 *		=============================
 *
 * Bugs:
 *	Not reentrant.
 *
 *	Assumes the incoming stream is ASCII, rather than a local file
 *	format, and so ALWAYS converts from ASCII on non-ASCII machines.
 *	Therefore, non-ASCII machines can't read local files.
 *
 */

#define HTSTREAM_INTERNAL 1

#include <HTUtils.h>

/* Implements:
*/
#include <HTFormat.h>

static float HTMaxSecs = 1e10;	/* No effective limit */

#ifdef UNIX
#ifdef NeXT
#define PRESENT_POSTSCRIPT "open %s; /bin/rm -f %s\n"
#else
#define PRESENT_POSTSCRIPT "(ghostview %s ; /bin/rm -f %s)&\n"
			   /* Full pathname would be better! */
#endif /* NeXT */
#endif /* UNIX */

#include <HTML.h>
#include <HTMLDTD.h>
#include <HText.h>
#include <HTAlert.h>
#include <HTList.h>
#include <HTInit.h>
#include <HTTCP.h>
#include <HTTP.h>
/*	Streams and structured streams which we use:
*/
#include <HTFWriter.h>
#include <HTPlain.h>
#include <SGML.h>
#include <HTMLGen.h>

#include <LYexit.h>
#include <LYUtils.h>
#include <GridText.h>
#include <LYGlobalDefs.h>
#include <LYLeaks.h>

#ifdef DISP_PARTIAL
#include <LYMainLoop.h>
#endif

BOOL HTOutputSource = NO;	/* Flag: shortcut parser to stdout */

/* this version used by the NetToText stream */
struct _HTStream {
    const HTStreamClass *isa;
    BOOL had_cr;
    HTStream *sink;
};

/*	Presentation methods
 *	--------------------
 */
HTList *HTPresentations = NULL;
HTPresentation *default_presentation = NULL;

/*
 *	To free off the presentation list.
 */
#ifdef LY_FIND_LEAKS
static void HTFreePresentations(void);
#endif

/*	Define a presentation system command for a content-type
 *	-------------------------------------------------------
 */
void HTSetPresentation(const char *representation,
		       const char *command,
		       const char *testcommand,
		       double quality,
		       double secs,
		       double secs_per_byte,
		       long int maxbytes,
		       AcceptMedia media)
{
    HTPresentation *pres = typecalloc(HTPresentation);

    if (pres == NULL)
	outofmem(__FILE__, "HTSetPresentation");

    assert(representation != NULL);

    CTRACE2(TRACE_CFG,
	    (tfp,
	     "HTSetPresentation rep=%s, command=%s, test=%s, qual=%f\n",
	     NonNull(representation),
	     NonNull(command),
	     NonNull(testcommand),
	     quality));

    pres->rep = HTAtom_for(representation);
    pres->rep_out = WWW_PRESENT;	/* Fixed for now ... :-) */
    pres->converter = HTSaveAndExecute;		/* Fixed for now ...     */
    pres->quality = (float) quality;
    pres->secs = (float) secs;
    pres->secs_per_byte = (float) secs_per_byte;
    pres->maxbytes = maxbytes;
    pres->get_accept = 0;
    pres->accept_opt = media;

    pres->command = NULL;
    StrAllocCopy(pres->command, command);

    pres->testcommand = NULL;
    StrAllocCopy(pres->testcommand, testcommand);

    /*
     * Memory leak fixed.
     * 05-28-94 Lynx 2-3-1 Garrett Arch Blythe
     */
    if (!HTPresentations) {
	HTPresentations = HTList_new();
#ifdef LY_FIND_LEAKS
	atexit(HTFreePresentations);
#endif
    }

    if (strcmp(representation, "*") == 0) {
	FREE(default_presentation);
	default_presentation = pres;
    } else {
	HTList_addObject(HTPresentations, pres);
    }
}

/*	Define a built-in function for a content-type
 *	---------------------------------------------
 */
void HTSetConversion(const char *representation_in,
		     const char *representation_out,
		     HTConverter *converter,
		     double quality,
		     double secs,
		     double secs_per_byte,
		     long int maxbytes,
		     AcceptMedia media)
{
    HTPresentation *pres = typecalloc(HTPresentation);

    if (pres == NULL)
	outofmem(__FILE__, "HTSetConversion");

    CTRACE2(TRACE_CFG,
	    (tfp,
	     "HTSetConversion rep_in=%s, rep_out=%s, qual=%f\n",
	     NonNull(representation_in),
	     NonNull(representation_out),
	     quality));

    pres->rep = HTAtom_for(representation_in);
    pres->rep_out = HTAtom_for(representation_out);
    pres->converter = converter;
    pres->command = NULL;
    pres->testcommand = NULL;
    pres->quality = (float) quality;
    pres->secs = (float) secs;
    pres->secs_per_byte = (float) secs_per_byte;
    pres->maxbytes = maxbytes;
    pres->get_accept = TRUE;
    pres->accept_opt = media;

    /*
     * Memory Leak fixed.
     * 05-28-94 Lynx 2-3-1 Garrett Arch Blythe
     */
    if (!HTPresentations) {
	HTPresentations = HTList_new();
#ifdef LY_FIND_LEAKS
	atexit(HTFreePresentations);
#endif
    }

    HTList_addObject(HTPresentations, pres);
}

#ifdef LY_FIND_LEAKS
/*
 *	Purpose:	Free the presentation list.
 *	Arguments:	void
 *	Return Value:	void
 *	Remarks/Portability/Dependencies/Restrictions:
 *		Made to clean up Lynx's bad leakage.
 *	Revision History:
 *		05-28-94	created Lynx 2-3-1 Garrett Arch Blythe
 */
static void HTFreePresentations(void)
{
    HTPresentation *pres = NULL;

    /*
     * Loop through the list.
     */
    while (!HTList_isEmpty(HTPresentations)) {
	/*
	 * Free off each item.  May also need to free off it's items, but not
	 * sure as of yet.
	 */
	pres = (HTPresentation *) HTList_removeLastObject(HTPresentations);
	FREE(pres->command);
	FREE(pres->testcommand);
	FREE(pres);
    }
    /*
     * Free the list itself.
     */
    HTList_delete(HTPresentations);
    HTPresentations = NULL;
}
#endif /* LY_FIND_LEAKS */

/*	File buffering
 *	--------------
 *
 *	The input file is read using the macro which can read from
 *	a socket or a file.
 *	The input buffer size, if large will give greater efficiency and
 *	release the server faster, and if small will save space on PCs etc.
 */
#define INPUT_BUFFER_SIZE 4096	/* Tradeoff */
static char input_buffer[INPUT_BUFFER_SIZE];
static char *input_pointer;
static char *input_limit;
static int input_file_number;

/*	Set up the buffering
 *
 *	These routines are public because they are in fact needed by
 *	many parsers, and on PCs and Macs we should not duplicate
 *	the static buffer area.
 */
void HTInitInput(int file_number)
{
    input_file_number = file_number;
    input_pointer = input_limit = input_buffer;
}

int interrupted_in_htgetcharacter = 0;
int HTGetCharacter(void)
{
    char ch;

    interrupted_in_htgetcharacter = 0;
    do {
	if (input_pointer >= input_limit) {
	    int status = NETREAD(input_file_number,
				 input_buffer, INPUT_BUFFER_SIZE);

	    if (status <= 0) {
		if (status == 0)
		    return EOF;
		if (status == HT_INTERRUPTED) {
		    CTRACE((tfp, "HTFormat: Interrupted in HTGetCharacter\n"));
		    interrupted_in_htgetcharacter = 1;
		    return EOF;
		}
		CTRACE((tfp, "HTFormat: File read error %d\n", status));
		return EOF;	/* -1 is returned by UCX
				   at end of HTTP link */
	    }
	    input_pointer = input_buffer;
	    input_limit = input_buffer + status;
	}
	ch = *input_pointer++;
    } while (ch == (char) 13);	/* Ignore ASCII carriage return */

    return FROMASCII(UCH(ch));
}

#ifdef USE_SSL
int HTGetSSLCharacter(void *handle)
{
    char ch;

    interrupted_in_htgetcharacter = 0;
    if (!handle)
	return (char) EOF;
    do {
	if (input_pointer >= input_limit) {
	    int status = SSL_read((SSL *) handle,
				  input_buffer, INPUT_BUFFER_SIZE);

	    if (status <= 0) {
		if (status == 0)
		    return (char) EOF;
		if (status == HT_INTERRUPTED) {
		    CTRACE((tfp,
			    "HTFormat: Interrupted in HTGetSSLCharacter\n"));
		    interrupted_in_htgetcharacter = 1;
		    return (char) EOF;
		}
		CTRACE((tfp, "HTFormat: SSL_read error %d\n", status));
		return (char) EOF;	/* -1 is returned by UCX
					   at end of HTTP link */
	    }
	    input_pointer = input_buffer;
	    input_limit = input_buffer + status;
	}
	ch = *input_pointer++;
    } while (ch == (char) 13);	/* Ignore ASCII carriage return */

    return FROMASCII(ch);
}
#endif /* USE_SSL */

/* Match maintype to any MIME type starting with maintype, for example: 
 * image/gif should match image
 */
static int half_match(char *trial_type, char *target)
{
    char *cp = StrChr(trial_type, '/');

    /* if no '/' or no '*' */
    if (!cp || *(cp + 1) != '*')
	return 0;

    CTRACE((tfp, "HTFormat: comparing %s and %s for half match\n",
	    trial_type, target));

    /* main type matches */
    if (!StrNCmp(trial_type, target, ((cp - trial_type) - 1)))
	return 1;

    return 0;
}

/*
 * Evaluate a deferred mailcap test command, i.e.,. one that substitutes the
 * document's charset or other values in %{name} format.
 */
static BOOL failsMailcap(HTPresentation *pres, HTParentAnchor *anchor)
{
    if (pres->testcommand != 0) {
	if (LYTestMailcapCommand(pres->testcommand,
				 anchor->content_type_params) != 0)
	    return TRUE;
    }
    return FALSE;
}

#define WWW_WILDCARD_REP_OUT HTAtom_for("*")

/*		Look up a presentation
 *		----------------------
 *
 *	If fill_in is NULL, only look for an exact match.
 *	If a wildcard match is made, *fill_in is used to store
 *	a possibly modified presentation, and a pointer to it is
 *	returned.  For an exact match, a pointer to the presentation
 *	in the HTPresentations list is returned.  Returns NULL if
 *	nothing found. - kw
 *
 */
static HTPresentation *HTFindPresentation(HTFormat rep_in,
					  HTFormat rep_out,
					  HTPresentation *fill_in,
					  HTParentAnchor *anchor)
{
    HTAtom *wildcard = NULL;	/* = HTAtom_for("*"); lookup when needed - kw */
    int n;
    int i;
    HTPresentation *pres;
    HTPresentation *match;
    HTPresentation *strong_wildcard_match = 0;
    HTPresentation *weak_wildcard_match = 0;
    HTPresentation *last_default_match = 0;
    HTPresentation *strong_subtype_wildcard_match = 0;

    CTRACE((tfp, "HTFormat: Looking up presentation for %s to %s\n",
	    HTAtom_name(rep_in), HTAtom_name(rep_out)));

    n = HTList_count(HTPresentations);
    for (i = 0; i < n; i++) {
	pres = (HTPresentation *) HTList_objectAt(HTPresentations, i);
	if (pres->rep == rep_in) {
	    if (pres->rep_out == rep_out) {
		if (failsMailcap(pres, anchor))
		    continue;
		CTRACE((tfp, "FindPresentation: found exact match: %s -> %s\n",
			HTAtom_name(pres->rep),
			HTAtom_name(pres->rep_out)));
		return pres;

	    } else if (!fill_in) {
		continue;
	    } else {
		if (!wildcard)
		    wildcard = WWW_WILDCARD_REP_OUT;
		if (pres->rep_out == wildcard) {
		    if (failsMailcap(pres, anchor))
			continue;
		    if (!strong_wildcard_match)
			strong_wildcard_match = pres;
		    /* otherwise use the first one */
		    CTRACE((tfp,
			    "StreamStack: found strong wildcard match: %s -> %s\n",
			    HTAtom_name(pres->rep),
			    HTAtom_name(pres->rep_out)));
		}
	    }

	} else if (!fill_in) {
	    continue;

	} else if (half_match(HTAtom_name(pres->rep),
			      HTAtom_name(rep_in))) {
	    if (pres->rep_out == rep_out) {
		if (failsMailcap(pres, anchor))
		    continue;
		if (!strong_subtype_wildcard_match)
		    strong_subtype_wildcard_match = pres;
		/* otherwise use the first one */
		CTRACE((tfp,
			"StreamStack: found strong subtype wildcard match: %s -> %s\n",
			HTAtom_name(pres->rep),
			HTAtom_name(pres->rep_out)));
	    }
	}

	if (pres->rep == WWW_SOURCE) {
	    if (pres->rep_out == rep_out) {
		if (failsMailcap(pres, anchor))
		    continue;
		if (!weak_wildcard_match)
		    weak_wildcard_match = pres;
		/* otherwise use the first one */
		CTRACE((tfp,
			"StreamStack: found weak wildcard match: %s\n",
			HTAtom_name(pres->rep_out)));

	    } else if (!last_default_match) {
		if (!wildcard)
		    wildcard = WWW_WILDCARD_REP_OUT;
		if (pres->rep_out == wildcard) {
		    if (failsMailcap(pres, anchor))
			continue;
		    last_default_match = pres;
		    /* otherwise use the first one */
		}
	    }
	}
    }

    match = (strong_subtype_wildcard_match
	     ? strong_subtype_wildcard_match
	     : (strong_wildcard_match
		? strong_wildcard_match
		: (weak_wildcard_match
		   ? weak_wildcard_match
		   : last_default_match)));

    if (match) {
	*fill_in = *match;	/* Specific instance */
	fill_in->rep = rep_in;	/* yuk */
	fill_in->rep_out = rep_out;	/* yuk */
	return fill_in;
    }

    return NULL;
}

/*		Create a filter stack
 *		---------------------
 *
 *	If a wildcard match is made, a temporary HTPresentation
 *	structure is made to hold the destination format while the
 *	new stack is generated. This is just to pass the out format to
 *	MIME so far.  Storing the format of a stream in the stream might
 *	be a lot neater.
 *
 */
HTStream *HTStreamStack(HTFormat rep_in,
			HTFormat rep_out,
			HTStream *sink,
			HTParentAnchor *anchor)
{
    HTPresentation temp;
    HTPresentation *match;
    HTStream *result;

    CTRACE((tfp, "StreamStack: Constructing stream stack for %s to %s (%s)\n",
	    HTAtom_name(rep_in),
	    HTAtom_name(rep_out),
	    NONNULL(anchor->content_type_params)));

    if (rep_out == rep_in) {
	result = sink;

    } else if ((match = HTFindPresentation(rep_in, rep_out, &temp, anchor))) {
	if (match == &temp) {
	    CTRACE((tfp, "StreamStack: Using %s\n", HTAtom_name(temp.rep_out)));
	} else {
	    CTRACE((tfp, "StreamStack: found exact match: %s -> %s\n",
		    HTAtom_name(match->rep),
		    HTAtom_name(match->rep_out)));
	}
	result = (*match->converter) (match, anchor, sink);
    } else {
	result = NULL;
    }
    if (TRACE) {
	if (result && result->isa && result->isa->name) {
	    CTRACE((tfp, "StreamStack: Returning \"%s\"\n", result->isa->name));
	} else if (result) {
	    CTRACE((tfp, "StreamStack: Returning *unknown* stream!\n"));
	} else {
	    CTRACE((tfp, "StreamStack: Returning NULL!\n"));
	    CTRACE_FLUSH(tfp);	/* a crash may be imminent... - kw */
	}
    }
    return result;
}

/*		Put a presentation near start of list
 *		-------------------------------------
 *
 *	Look up a presentation (exact match only) and, if found, reorder
 *	it to the start of the HTPresentations list. - kw
 */
void HTReorderPresentation(HTFormat rep_in,
			   HTFormat rep_out)
{
    HTPresentation *match;

    if ((match = HTFindPresentation(rep_in, rep_out, NULL, NULL))) {
	HTList_removeObject(HTPresentations, match);
	HTList_addObject(HTPresentations, match);
    }
}

/*
 * Setup 'get_accept' flag to denote presentations that are not redundant,
 * and will be listed in "Accept:" header.
 */
void HTFilterPresentations(void)
{
    int i, j;
    int n = HTList_count(HTPresentations);
    HTPresentation *p, *q;
    BOOL matched;
    char *s, *t;

    CTRACE((tfp, "HTFilterPresentations (AcceptMedia %#x)\n", LYAcceptMedia));
    for (i = 0; i < n; i++) {
	p = (HTPresentation *) HTList_objectAt(HTPresentations, i);
	s = HTAtom_name(p->rep);

	p->get_accept = FALSE;
	if ((LYAcceptMedia & p->accept_opt) != 0
	    && p->rep_out == WWW_PRESENT
	    && p->rep != WWW_SOURCE
	    && strcasecomp(s, "www/mime")
	    && strcasecomp(s, "www/compressed")
	    && p->quality <= 1.0 && p->quality >= 0.0) {
	    matched = TRUE;
	    for (j = 0; j < i; j++) {
		q = (HTPresentation *) HTList_objectAt(HTPresentations, j);
		t = HTAtom_name(q->rep);

		if (!strcasecomp(s, t)) {
		    matched = FALSE;
		    CTRACE((tfp, "  match %s %s\n", s, t));
		    break;
		}
	    }
	    p->get_accept = matched;
	}
    }
}

/*		Find the cost of a filter stack
 *		-------------------------------
 *
 *	Must return the cost of the same stack which StreamStack would set up.
 *
 * On entry,
 *	length	The size of the data to be converted
 */
float HTStackValue(HTFormat rep_in,
		   HTFormat rep_out,
		   double initial_value,
		   long int length)
{
    HTAtom *wildcard = WWW_WILDCARD_REP_OUT;

    CTRACE((tfp, "HTFormat: Evaluating stream stack for %s worth %.3f to %s\n",
	    HTAtom_name(rep_in), initial_value, HTAtom_name(rep_out)));

    if (rep_out == WWW_SOURCE || rep_out == rep_in)
	return 0.0;

    {
	int n = HTList_count(HTPresentations);
	int i;
	HTPresentation *pres;

	for (i = 0; i < n; i++) {
	    pres = (HTPresentation *) HTList_objectAt(HTPresentations, i);
	    if (pres->rep == rep_in &&
		(pres->rep_out == rep_out || pres->rep_out == wildcard)) {
		float value = (float) (initial_value * pres->quality);

		if (HTMaxSecs > 0.0)
		    value = (value
			     - ((float) length * pres->secs_per_byte
				+ pres->secs)
			     / HTMaxSecs);
		return value;
	    }
	}
    }

    return (float) -1e30;	/* Really bad */

}

/*	Display the page while transfer in progress
 *	-------------------------------------------
 *
 *   Repaint the page only when necessary.
 *   This is a traverse call for HText_pageDisplay() - it works!.
 *
 */
void HTDisplayPartial(void)
{
#ifdef DISP_PARTIAL
    if (display_partial) {
	/*
	 * HText_getNumOfLines() = "current" number of complete lines received
	 * NumOfLines_partial = number of lines at the moment of last repaint. 
	 * (we update NumOfLines_partial only when we repaint the display.)
	 *
	 * display_partial could only be enabled in HText_new() so a new
	 * HTMainText object available - all HText_ functions use it, lines
	 * counter HText_getNumOfLines() in particular.
	 *
	 * Otherwise HTMainText holds info from the previous document and we
	 * may repaint it instead of the new one:  prev doc scrolled to the
	 * first line (=Newline_partial) is not good looking :-) 23 Aug 1998
	 * Leonid Pauzner
	 *
	 * So repaint the page only when necessary:
	 */
	int Newline_partial = LYGetNewline();

	if (((Newline_partial + display_lines) - 1 > NumOfLines_partial)
	/* current page not complete... */
	    && (partial_threshold > 0 ?
		((Newline_partial + partial_threshold) - 1 <=
		 HText_getNumOfLines()) :
		((Newline_partial + display_lines) - 1 <= HText_getNumOfLines()))
	/*
	 * Originally we rendered by increments of 2 lines,
	 * but that got annoying on slow network connections.
	 * Then we switched to full-pages.  Now it's configurable.
	 * If partial_threshold <= 0, then it's a full page
	 */
	    ) {
	    if (LYMainLoop_pageDisplay(Newline_partial))
		NumOfLines_partial = HText_getNumOfLines();
	}
    }
#else /* nothing */
#endif /* DISP_PARTIAL */
}

/* Put this as early as possible, OK just after HTDisplayPartial() */
void HTFinishDisplayPartial(void)
{
#ifdef DISP_PARTIAL
    /*
     * End of incremental rendering stage here.
     */
    display_partial = FALSE;
#endif /* DISP_PARTIAL */
}

/*	Push data from a socket down a stream
 *	-------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *   The file number given is assumed to be a TELNET stream, i.e., containing
 *   CRLF at the end of lines which need to be stripped to LF for unix
 *   when the format is textual.
 *
 *  State of socket and target stream on entry:
 *			socket (file_number) assumed open,
 *			target (sink) assumed valid.
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption or error after some data received.
 *	-2		Unexpected disconnect before any data received.
 *	-1		Interruption or error before any data received, or
 *			(UNIX) other read error before any data received, or
 *			download cancelled.
 *	HT_LOADED	Normal close of socket (end of file indication
 *			received), or
 *			unexpected disconnect after some data received, or
 *			other read error after some data received, or
 *			(not UNIX) other read error before any data received.
 *
 *  State of socket and target stream on return depends on return value:
 *	HT_INTERRUPTED	socket still open, target aborted.
 *	-2		socket still open, target stream still valid.
 *	-1		socket still open, target aborted.
 *	otherwise	socket closed,	target stream still valid.
 */
int HTCopy(HTParentAnchor *anchor,
	   int file_number,
	   void *handle GCC_UNUSED,
	   HTStream *sink)
{
    HTStreamClass targetClass;
    BOOL suppress_readprogress = NO;
    off_t limit = anchor ? anchor->content_length : 0;
    off_t bytes = 0;
    off_t header_length = 0;
    int rv = 0;

    /*  Push the data down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    /*
     * Push binary from socket down sink
     *
     * This operation could be put into a main event loop
     */
    HTReadProgress(bytes, (off_t) 0);
    for (;;) {
	int status;

	if (LYCancelDownload) {
	    LYCancelDownload = FALSE;
	    (*targetClass._abort) (sink, NULL);
	    rv = -1;
	    goto finished;
	}

	if (HTCheckForInterrupt()) {
	    _HTProgress(TRANSFER_INTERRUPTED);
	    (*targetClass._abort) (sink, NULL);
	    if (bytes)
		rv = HT_INTERRUPTED;
	    else
		rv = -1;
	    goto finished;
	}
#ifdef USE_SSL
	if (handle)
	    status = SSL_read((SSL *) handle, input_buffer, INPUT_BUFFER_SIZE);
	else
	    status = NETREAD(file_number, input_buffer, INPUT_BUFFER_SIZE);
#else
	status = NETREAD(file_number, input_buffer, INPUT_BUFFER_SIZE);
#endif /* USE_SSL */
	if (status <= 0) {
	    if (status == 0) {
		break;
	    } else if (status == HT_INTERRUPTED) {
		_HTProgress(TRANSFER_INTERRUPTED);
		(*targetClass._abort) (sink, NULL);
		if (bytes)
		    rv = HT_INTERRUPTED;
		else
		    rv = -1;
		goto finished;
	    } else if (SOCKET_ERRNO == ENOTCONN ||
#ifdef _WINDOWS			/* 1997/11/10 (Mon) 16:57:18 */
		       SOCKET_ERRNO == ETIMEDOUT ||
#endif
		       SOCKET_ERRNO == ECONNRESET ||
		       SOCKET_ERRNO == EPIPE) {
		/*
		 * Arrrrgh, HTTP 0/1 compatibility problem, maybe.
		 */
		if (bytes <= 0) {
		    /*
		     * Don't have any data, so let the calling function decide
		     * what to do about it.  - FM
		     */
		    rv = -2;
		    goto finished;
		} else {
#ifdef UNIX
		    /*
		     * Treat what we've received already as the complete
		     * transmission, but not without giving the user an alert. 
		     * I don't know about all the different TCP stacks for VMS
		     * etc., so this is currently only for UNIX.  - kw
		     */
		    HTInetStatus("NETREAD");
		    HTAlert("Unexpected server disconnect.");
		    CTRACE((tfp,
			    "HTCopy: Unexpected server disconnect. Treating as completed.\n"));
#else /* !UNIX */
		    /*
		     * Treat what we've gotten already as the complete
		     * transmission.  - FM
		     */
		    CTRACE((tfp,
			    "HTCopy: Unexpected server disconnect.  Treating as completed.\n"));
		    status = 0;
#endif /* UNIX */
		}
#ifdef UNIX
	    } else {		/* status < 0 and other errno */
		/*
		 * Treat what we've received already as the complete
		 * transmission, but not without giving the user an alert.  I
		 * don't know about all the different TCP stacks for VMS etc.,
		 * so this is currently only for UNIX.  - kw
		 */
		HTInetStatus("NETREAD");
		HTAlert("Unexpected read error.");
		if (bytes) {
		    (void) NETCLOSE(file_number);
		    rv = HT_LOADED;
		} else {
		    (*targetClass._abort) (sink, NULL);
		    rv = -1;
		}
		goto finished;
#endif
	    }
	    break;
	}

	/*
	 * Suppress ReadProgress messages when collecting a redirection
	 * message, at least initially (unless/until anchor->content_type gets
	 * changed, probably by the MIME message parser).  That way messages
	 * put up by the HTTP module or elsewhere can linger in the statusline
	 * for a while.  - kw
	 */
	suppress_readprogress = (BOOL) (anchor && anchor->content_type &&
					!strcmp(anchor->content_type,
						"message/x-http-redirection"));
#ifdef NOT_ASCII
	{
	    char *p;

	    for (p = input_buffer; p < input_buffer + status; p++) {
		*p = FROMASCII(*p);
	    }
	}
#endif /* NOT_ASCII */

	header_length = anchor != 0 ? anchor->header_length : 0;

	(*targetClass.put_block) (sink, input_buffer, status);
	if (anchor != 0 && anchor->inHEAD) {
	    if (!suppress_readprogress) {
		statusline(gettext("Reading headers..."));
	    }
	    CTRACE((tfp, "HTCopy read %" PRI_off_t " header bytes\n",
		    CAST_off_t (anchor->header_length)));
	} else {
	    /*
	     * If header-length is increased at this point, that is due to
	     * HTMIME, which detects the end of the server headers.  There
	     * may be additional (non-header) data in that block.
	     */
	    if (anchor != 0 && (anchor->header_length > header_length)) {
		int header = (int) (anchor->header_length - header_length);

		CTRACE((tfp, "HTCopy read %" PRI_off_t " header bytes "
			"(%d extra vs %d total)\n",
			CAST_off_t (anchor->header_length),
			header, status));
		if (status > header) {
		    bytes += (status - header);
		}
	    } else {
		bytes += status;
	    }
	    if (!suppress_readprogress) {
		HTReadProgress(bytes, limit);
	    }
	    HTDisplayPartial();
	}

	/* a few buggy implementations do not close the connection properly
	 * and will hang if we try to read past the declared content-length.
	 */
	if (limit > 0 && bytes >= limit)
	    break;
    }				/* next bufferload */
    if (anchor != 0) {
	CTRACE((tfp, "HTCopy copied %"
		PRI_off_t " actual, %"
		PRI_off_t " limit\n", CAST_off_t (bytes), CAST_off_t (limit)));
	anchor->actual_length = bytes;
    }

    _HTProgress(TRANSFER_COMPLETE);
    (void) NETCLOSE(file_number);
    rv = HT_LOADED;

  finished:
    HTFinishDisplayPartial();
    return (rv);
}

/*	Push data from a file pointer down a stream
 *	-------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *
 *  State of file and target stream on entry:
 *			FILE* (fp) assumed open,
 *			target (sink) assumed valid.
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption after some data read.
 *	HT_PARTIAL_CONTENT	Error after some data read.
 *	-1		Error before any data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		fp still open, target stream still valid.
 */
int HTFileCopy(FILE *fp, HTStream *sink)
{
    HTStreamClass targetClass;
    int status;
    off_t bytes;
    int rv = HT_OK;

    /*  Push the data down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    /*  Push binary from socket down sink
     */
    HTReadProgress(bytes = 0, (off_t) 0);
    for (;;) {
	status = (int) fread(input_buffer,
			     (size_t) 1,
			     (size_t) INPUT_BUFFER_SIZE, fp);
	if (status == 0) {	/* EOF or error */
	    if (ferror(fp) == 0) {
		rv = HT_LOADED;
		break;
	    }
	    CTRACE((tfp, "HTFormat: Read error, read returns %d\n",
		    ferror(fp)));
	    if (bytes) {
		rv = HT_PARTIAL_CONTENT;
	    } else {
		rv = -1;
	    }
	    break;
	}

	(*targetClass.put_block) (sink, input_buffer, status);
	bytes += status;
	HTReadProgress(bytes, (off_t) 0);
	/* Suppress last screen update in partial mode - a regular update under
	 * control of mainloop() should follow anyway.  - kw
	 */
#ifdef DISP_PARTIAL
	if (display_partial && bytes != HTMainAnchor->content_length)
	    HTDisplayPartial();
#endif

	if (HTCheckForInterrupt()) {
	    _HTProgress(TRANSFER_INTERRUPTED);
	    if (bytes) {
		rv = HT_INTERRUPTED;
	    } else {
		rv = -1;
	    }
	    break;
	}
    }				/* next bufferload */

    HTFinishDisplayPartial();
    return rv;
}

#ifdef USE_SOURCE_CACHE
/*	Push data from an HTChunk down a stream
 *	---------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *  State of memory and target stream on entry:
 *			HTChunk* (chunk) and target (sink) assumed valid.
 *
 *  Return values:
 *	HT_LOADED	All data sent.
 *	HT_INTERRUPTED  Interruption after some data read.
 *
 *  State of memory and target stream on return:
 *	always		chunk unchanged, target stream still valid.
 */
int HTMemCopy(HTChunk *chunk, HTStream *sink)
{
    HTStreamClass targetClass;
    off_t bytes;
    int rv = HT_OK;

    targetClass = *(sink->isa);
    HTReadProgress(bytes = 0, (off_t) 0);
    for (; chunk != NULL; chunk = chunk->next) {

	/* Push the data down the stream a piece at a time, in case we're
	 * running a large document on a slow machine.
	 */
	(*targetClass.put_block) (sink, chunk->data, chunk->size);
	bytes += chunk->size;

	HTReadProgress(bytes, (off_t) 0);
	HTDisplayPartial();

	if (HTCheckForInterrupt()) {
	    _HTProgress(TRANSFER_INTERRUPTED);
	    if (bytes) {
		rv = HT_INTERRUPTED;
	    } else {
		rv = -1;
	    }
	    break;
	}
    }

    HTFinishDisplayPartial();
    return rv;
}
#endif

#ifdef USE_ZLIB
/*	Push data from a gzip file pointer down a stream
 *	-------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *
 *  State of file and target stream on entry:
 *		      gzFile (gzfp) assumed open (should have gzipped content),
 *		      target (sink) assumed valid.
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption after some data read.
 *	HT_PARTIAL_CONTENT	Error after some data read.
 *	-1		Error before any data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		gzfp still open, target stream still valid.
 */
static int HTGzFileCopy(gzFile gzfp, HTStream *sink)
{
    HTStreamClass targetClass;
    int status;
    off_t bytes;
    int gzerrnum;
    int rv = HT_OK;

    /*  Push the data down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    /*  read and inflate gzip'd file, and push binary down sink
     */
    HTReadProgress(bytes = 0, (off_t) 0);
    for (;;) {
	status = gzread(gzfp, input_buffer, INPUT_BUFFER_SIZE);
	if (status <= 0) {	/* EOF or error */
	    if (status == 0) {
		rv = HT_LOADED;
		break;
	    }
	    CTRACE((tfp, "HTGzFileCopy: Read error, gzread returns %d\n",
		    status));
	    CTRACE((tfp, "gzerror   : %s\n",
		    gzerror(gzfp, &gzerrnum)));
	    if (TRACE) {
		if (gzerrnum == Z_ERRNO)
		    perror("gzerror   ");
	    }
	    if (bytes) {
		rv = HT_PARTIAL_CONTENT;
	    } else {
		rv = -1;
	    }
	    break;
	}

	(*targetClass.put_block) (sink, input_buffer, status);
	bytes += status;
	HTReadProgress(bytes, (off_t) -1);
	HTDisplayPartial();

	if (HTCheckForInterrupt()) {
	    _HTProgress(TRANSFER_INTERRUPTED);
	    rv = HT_INTERRUPTED;
	    break;
	}
    }				/* next bufferload */

    HTFinishDisplayPartial();
    return rv;
}

#ifndef HAVE_ZERROR
#define zError(s) LynxZError(s)
static const char *zError(int status)
{
    static char result[80];

    sprintf(result, "zlib error %d", status);
    return result;
}
#endif

/*	Push data from a deflate file pointer down a stream
 *	-------------------------------------
 *
 *  This routine is responsible for creating and PRESENTING any
 *  graphic (or other) objects described by the file.  The code is
 *  loosely based on the inflate.c file from w3m.
 *
 *
 *  State of file and target stream on entry:
 *		      FILE (zzfp) assumed open (should have deflated content),
 *		      target (sink) assumed valid.
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption after some data read.
 *	HT_PARTIAL_CONTENT	Error after some data read.
 *	-1		Error before any data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		zzfp still open, target stream still valid.
 */
static int HTZzFileCopy(FILE *zzfp, HTStream *sink)
{
    static char dummy_head[1 + 1] =
    {
	0x8 + 0x7 * 0x10,
	(((0x8 + 0x7 * 0x10) * 0x100 + 30) / 31 * 31) & 0xFF,
    };

    z_stream s;
    HTStreamClass targetClass;
    off_t bytes;
    int rv = HT_OK;
    char output_buffer[INPUT_BUFFER_SIZE];
    int status;
    int flush;
    int retry = 0;
    int len = 0;

    /*  Push the data down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    s.zalloc = Z_NULL;
    s.zfree = Z_NULL;
    s.opaque = Z_NULL;
    status = inflateInit(&s);
    if (status != Z_OK) {
	CTRACE((tfp, "HTZzFileCopy inflateInit() %s\n", zError(status)));
	exit_immediately(EXIT_FAILURE);
    }
    s.avail_in = 0;
    s.next_out = (Bytef *) output_buffer;
    s.avail_out = sizeof(output_buffer);
    flush = Z_NO_FLUSH;

    /*  read and inflate deflate'd file, and push binary down sink
     */
    HTReadProgress(bytes = 0, (off_t) 0);
    for (;;) {
	if (s.avail_in == 0) {
	    s.next_in = (Bytef *) input_buffer;
	    s.avail_in = (uInt) fread(input_buffer,
				      (size_t) 1,
				      (size_t) INPUT_BUFFER_SIZE, zzfp);
	    len = (int) s.avail_in;
	}
	status = inflate(&s, flush);
	if (status == Z_STREAM_END || status == Z_BUF_ERROR) {
	    len = (int) sizeof(output_buffer) - (int) s.avail_out;
	    if (len > 0) {
		(*targetClass.put_block) (sink, output_buffer, len);
		bytes += len;
		HTReadProgress(bytes, (off_t) -1);
		HTDisplayPartial();
	    }
	    rv = HT_LOADED;
	    break;
	} else if (status == Z_DATA_ERROR && !retry++) {
	    status = inflateReset(&s);
	    if (status != Z_OK) {
		CTRACE((tfp, "HTZzFileCopy inflateReset() %s\n", zError(status)));
		rv = -1;
		break;
	    }
	    s.next_in = (Bytef *) dummy_head;
	    s.avail_in = sizeof(dummy_head);
	    (void) inflate(&s, flush);
	    s.next_in = (Bytef *) input_buffer;
	    s.avail_in = (unsigned) len;
	    continue;
	} else if (status != Z_OK) {
	    CTRACE((tfp, "HTZzFileCopy inflate() %s\n", zError(status)));
	    rv = bytes ? HT_PARTIAL_CONTENT : -1;
	    break;
	} else if (s.avail_out == 0) {
	    len = sizeof(output_buffer);
	    s.next_out = (Bytef *) output_buffer;
	    s.avail_out = sizeof(output_buffer);

	    (*targetClass.put_block) (sink, output_buffer, len);
	    bytes += len;
	    HTReadProgress(bytes, (off_t) -1);
	    HTDisplayPartial();

	    if (HTCheckForInterrupt()) {
		_HTProgress(TRANSFER_INTERRUPTED);
		rv = bytes ? HT_INTERRUPTED : -1;
		break;
	    }
	}
	retry = 1;
    }				/* next bufferload */

    inflateEnd(&s);
    HTFinishDisplayPartial();
    return rv;
}
#endif /* USE_ZLIB */

#ifdef USE_BZLIB
/*	Push data from a bzip file pointer down a stream
 *	-------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *
 *  State of file and target stream on entry:
 *		      BZFILE (bzfp) assumed open (should have bzipped content),
 *		      target (sink) assumed valid.
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption after some data read.
 *	HT_PARTIAL_CONTENT	Error after some data read.
 *	-1		Error before any data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		bzfp still open, target stream still valid.
 */
static int HTBzFileCopy(BZFILE * bzfp, HTStream *sink)
{
    HTStreamClass targetClass;
    int status;
    off_t bytes;
    int bzerrnum;
    int rv = HT_OK;

    /*  Push the data down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    /*  read and inflate bzip'd file, and push binary down sink
     */
    HTReadProgress(bytes = 0, (off_t) 0);
    for (;;) {
	status = BZ2_bzread(bzfp, input_buffer, INPUT_BUFFER_SIZE);
	if (status <= 0) {	/* EOF or error */
	    if (status == 0) {
		rv = HT_LOADED;
		break;
	    }
	    CTRACE((tfp, "HTBzFileCopy: Read error, bzread returns %d\n",
		    status));
	    CTRACE((tfp, "bzerror   : %s\n",
		    BZ2_bzerror(bzfp, &bzerrnum)));
	    if (bytes) {
		rv = HT_PARTIAL_CONTENT;
	    } else {
		rv = -1;
	    }
	    break;
	}

	(*targetClass.put_block) (sink, input_buffer, status);
	bytes += status;
	HTReadProgress(bytes, (off_t) -1);
	HTDisplayPartial();

	if (HTCheckForInterrupt()) {
	    _HTProgress(TRANSFER_INTERRUPTED);
	    rv = HT_INTERRUPTED;
	    break;
	}
    }				/* next bufferload */

    HTFinishDisplayPartial();
    return rv;
}
#endif /* USE_BZLIB */

/*	Push data from a socket down a stream STRIPPING CR
 *	--------------------------------------------------
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the socket.
 *
 *   The file number given is assumed to be a TELNET stream ie containing
 *   CRLF at the end of lines which need to be stripped to LF for unix
 *   when the format is textual.
 *
 */
void HTCopyNoCR(HTParentAnchor *anchor GCC_UNUSED,
		int file_number,
		HTStream *sink)
{
    HTStreamClass targetClass;
    int character;

    /*  Push the data, ignoring CRLF, down the stream
     */
    targetClass = *(sink->isa);	/* Copy pointers to procedures */

    /*
     * Push text from telnet socket down sink
     *
     * @@@@@ To push strings could be faster?  (especially is we cheat and
     * don't ignore CR!  :-}
     */
    HTInitInput(file_number);
    for (;;) {
	character = HTGetCharacter();
	if (character == EOF)
	    break;
	(*targetClass.put_character) (sink, (char) character);
    }
}

/*	Parse a socket given format and file number
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *   The file number given is assumed to be a TELNET stream ie containing
 *   CRLF at the end of lines which need to be stripped to LF for unix
 *   when the format is textual.
 *
 *  State of socket and target stream on entry:
 *			socket (file_number) assumed open,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	HT_INTERRUPTED  Interruption or error after some data received.
 *	-501		Stream stack failed (cannot present or convert).
 *	-2		Unexpected disconnect before any data received.
 *	-1		Stream stack failed (cannot present or convert), or
 *			Interruption or error before any data received, or
 *			(UNIX) other read error before any data received, or
 *			download cancelled.
 *	HT_LOADED	Normal close of socket (end of file indication
 *			received), or
 *			unexpected disconnect after some data received, or
 *			other read error after some data received, or
 *			(not UNIX) other read error before any data received.
 *
 *  State of socket and target stream on return depends on return value:
 *	HT_INTERRUPTED	socket still open, target aborted.
 *	-501		socket still open, target stream NULL.
 *	-2		socket still open, target freed.
 *	-1		socket still open, target stream aborted or NULL.
 *	otherwise	socket closed,	target stream freed.
 */
int HTParseSocket(HTFormat rep_in,
		  HTFormat format_out,
		  HTParentAnchor *anchor,
		  int file_number,
		  HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;

    stream = HTStreamStack(rep_in, format_out, sink, anchor);

    if (!stream) {
	char *buffer = 0;

	if (LYCancelDownload) {
	    LYCancelDownload = FALSE;
	    return -1;
	}
	HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
		   HTAtom_name(rep_in), HTAtom_name(format_out));
	CTRACE((tfp, "HTFormat: %s\n", buffer));
	rv = HTLoadError(sink, 501, buffer);	/* returns -501 */
	FREE(buffer);
    } else {
	/*
	 * Push the data, don't worry about CRLF we can strip them later.
	 */
	targetClass = *(stream->isa);	/* Copy pointers to procedures */
	rv = HTCopy(anchor, file_number, NULL, stream);
	if (rv != -1 && rv != HT_INTERRUPTED)
	    (*targetClass._free) (stream);
    }
    return rv;
    /* Originally:  full: HT_LOADED;  partial: HT_INTERRUPTED;  no bytes: -1 */
}

/*	Parse a file given format and file pointer
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *   The file number given is assumed to be a TELNET stream ie containing
 *   CRLF at the end of lines which need to be stripped to \n for unix
 *   when the format is textual.
 *
 *  State of file and target stream on entry:
 *			FILE* (fp) assumed open,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	-501		Stream stack failed (cannot present or convert).
 *	-1		Download cancelled.
 *	HT_NO_DATA	Error before any data read.
 *	HT_PARTIAL_CONTENT	Interruption or error after some data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		fp still open; target freed, aborted, or NULL.
 */
int HTParseFile(HTFormat rep_in,
		HTFormat format_out,
		HTParentAnchor *anchor,
		FILE *fp,
		HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;
    int result;

    if (fp == NULL) {
	result = HT_LOADED;
    } else {
	stream = HTStreamStack(rep_in, format_out, sink, anchor);

	if (!stream || !stream->isa) {
	    char *buffer = 0;

	    if (LYCancelDownload) {
		LYCancelDownload = FALSE;
		result = -1;
	    } else {
		HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
			   HTAtom_name(rep_in), HTAtom_name(format_out));
		CTRACE((tfp, "HTFormat(in HTParseFile): %s\n", buffer));
		rv = HTLoadError(sink, 501, buffer);
		FREE(buffer);
		result = rv;
	    }
	} else {

	    /*
	     * Push the data down the stream
	     *
	     * @@ Bug:  This decision ought to be made based on "encoding"
	     * rather than on content-type.  @@@ When we handle encoding.  The
	     * current method smells anyway.
	     */
	    targetClass = *(stream->isa);	/* Copy pointers to procedures */
	    rv = HTFileCopy(fp, stream);
	    if (rv == -1 || rv == HT_INTERRUPTED) {
		(*targetClass._abort) (stream, NULL);
	    } else {
		(*targetClass._free) (stream);
	    }

	    if (rv == -1) {
		result = HT_NO_DATA;
	    } else if (rv == HT_INTERRUPTED || (rv > 0 && rv != HT_LOADED)) {
		result = HT_PARTIAL_CONTENT;
	    } else {
		result = HT_LOADED;
	    }
	}
    }
    return result;
}

#ifdef USE_SOURCE_CACHE
/*	Parse a document in memory given format and memory block pointer
 *
 *   This routine is responsible for creating and PRESENTING any
 *   graphic (or other) objects described by the file.
 *
 *  State of memory and target stream on entry:
 *			HTChunk* (chunk) assumed valid,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	-501		Stream stack failed (cannot present or convert).
 *	HT_LOADED	All data sent.
 *
 *  State of memory and target stream on return:
 *	always		chunk unchanged; target freed, aborted, or NULL.
 */
int HTParseMem(HTFormat rep_in,
	       HTFormat format_out,
	       HTParentAnchor *anchor,
	       HTChunk *chunk,
	       HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;
    int result;

    stream = HTStreamStack(rep_in, format_out, sink, anchor);
    if (!stream || !stream->isa) {
	char *buffer = 0;

	HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
		   HTAtom_name(rep_in), HTAtom_name(format_out));
	CTRACE((tfp, "HTFormat(in HTParseMem): %s\n", buffer));
	rv = HTLoadError(sink, 501, buffer);
	FREE(buffer);
	result = rv;
    } else {

	/* Push the data down the stream
	 */
	targetClass = *(stream->isa);
	(void) HTMemCopy(chunk, stream);
	(*targetClass._free) (stream);
	result = HT_LOADED;
    }
    return result;
}
#endif

#ifdef USE_ZLIB
static int HTCloseGzFile(gzFile gzfp)
{
    int gzres;

    if (gzfp == NULL)
	return 0;
    gzres = gzclose(gzfp);
    if (TRACE) {
	if (gzres == Z_ERRNO) {
	    perror("gzclose   ");
	} else if (gzres != Z_OK) {
	    CTRACE((tfp, "gzclose   : error number %d\n", gzres));
	}
    }
    return (gzres);
}

/*	HTParseGzFile
 *
 *  State of file and target stream on entry:
 *			gzFile (gzfp) assumed open,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	-501		Stream stack failed (cannot present or convert).
 *	-1		Download cancelled.
 *	HT_NO_DATA	Error before any data read.
 *	HT_PARTIAL_CONTENT	Interruption or error after some data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		gzfp closed; target freed, aborted, or NULL.
 */
int HTParseGzFile(HTFormat rep_in,
		  HTFormat format_out,
		  HTParentAnchor *anchor,
		  gzFile gzfp,
		  HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;
    int result;

    stream = HTStreamStack(rep_in, format_out, sink, anchor);

    if (!stream || !stream->isa) {
	char *buffer = 0;

	HTCloseGzFile(gzfp);
	if (LYCancelDownload) {
	    LYCancelDownload = FALSE;
	    result = -1;
	} else {
	    HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
		       HTAtom_name(rep_in), HTAtom_name(format_out));
	    CTRACE((tfp, "HTFormat(in HTParseGzFile): %s\n", buffer));
	    rv = HTLoadError(sink, 501, buffer);
	    FREE(buffer);
	    result = rv;
	}
    } else {

	/*
	 * Push the data down the stream
	 *
	 * @@ Bug:  This decision ought to be made based on "encoding" rather than
	 * on content-type.  @@@ When we handle encoding.  The current method
	 * smells anyway.
	 */
	targetClass = *(stream->isa);	/* Copy pointers to procedures */
	rv = HTGzFileCopy(gzfp, stream);
	if (rv == -1 || rv == HT_INTERRUPTED) {
	    (*targetClass._abort) (stream, NULL);
	} else {
	    (*targetClass._free) (stream);
	}

	HTCloseGzFile(gzfp);
	if (rv == -1) {
	    result = HT_NO_DATA;
	} else if (rv == HT_INTERRUPTED || (rv > 0 && rv != HT_LOADED)) {
	    result = HT_PARTIAL_CONTENT;
	} else {
	    result = HT_LOADED;
	}
    }
    return result;
}

/*	HTParseZzFile
 *
 *  State of file and target stream on entry:
 *			FILE (zzfp) assumed open,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	-501		Stream stack failed (cannot present or convert).
 *	-1		Download cancelled.
 *	HT_NO_DATA	Error before any data read.
 *	HT_PARTIAL_CONTENT	Interruption or error after some data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		zzfp closed; target freed, aborted, or NULL.
 */
int HTParseZzFile(HTFormat rep_in,
		  HTFormat format_out,
		  HTParentAnchor *anchor,
		  FILE *zzfp,
		  HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;
    int result;

    stream = HTStreamStack(rep_in, format_out, sink, anchor);

    if (!stream || !stream->isa) {
	char *buffer = 0;

	fclose(zzfp);
	if (LYCancelDownload) {
	    LYCancelDownload = FALSE;
	    result = -1;
	} else {
	    HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
		       HTAtom_name(rep_in), HTAtom_name(format_out));
	    CTRACE((tfp, "HTFormat(in HTParseGzFile): %s\n", buffer));
	    rv = HTLoadError(sink, 501, buffer);
	    FREE(buffer);
	    result = rv;
	}
    } else {

	/*
	 * Push the data down the stream
	 *
	 * @@ Bug:  This decision ought to be made based on "encoding" rather than
	 * on content-type.  @@@ When we handle encoding.  The current method
	 * smells anyway.
	 */
	targetClass = *(stream->isa);	/* Copy pointers to procedures */
	rv = HTZzFileCopy(zzfp, stream);
	if (rv == -1 || rv == HT_INTERRUPTED) {
	    (*targetClass._abort) (stream, NULL);
	} else {
	    (*targetClass._free) (stream);
	}

	fclose(zzfp);
	if (rv == -1) {
	    result = HT_NO_DATA;
	} else if (rv == HT_INTERRUPTED || (rv > 0 && rv != HT_LOADED)) {
	    result = HT_PARTIAL_CONTENT;
	} else {
	    result = HT_LOADED;
	}
    }
    return result;
}
#endif /* USE_ZLIB */

#ifdef USE_BZLIB
static void HTCloseBzFile(BZFILE * bzfp)
{
    if (bzfp)
	BZ2_bzclose(bzfp);
}

/*	HTParseBzFile
 *
 *  State of file and target stream on entry:
 *			bzFile (bzfp) assumed open,
 *			target (sink) usually NULL (will call stream stack).
 *
 *  Return values:
 *	-501		Stream stack failed (cannot present or convert).
 *	-1		Download cancelled.
 *	HT_NO_DATA	Error before any data read.
 *	HT_PARTIAL_CONTENT	Interruption or error after some data read.
 *	HT_LOADED	Normal end of file indication on reading.
 *
 *  State of file and target stream on return:
 *	always		bzfp closed; target freed, aborted, or NULL.
 */
int HTParseBzFile(HTFormat rep_in,
		  HTFormat format_out,
		  HTParentAnchor *anchor,
		  BZFILE * bzfp,
		  HTStream *sink)
{
    HTStream *stream;
    HTStreamClass targetClass;
    int rv;
    int result;

    stream = HTStreamStack(rep_in, format_out, sink, anchor);

    if (!stream || !stream->isa) {
	char *buffer = 0;

	HTCloseBzFile(bzfp);
	if (LYCancelDownload) {
	    LYCancelDownload = FALSE;
	    result = -1;
	} else {
	    HTSprintf0(&buffer, CANNOT_CONVERT_I_TO_O,
		       HTAtom_name(rep_in), HTAtom_name(format_out));
	    CTRACE((tfp, "HTFormat(in HTParseBzFile): %s\n", buffer));
	    rv = HTLoadError(sink, 501, buffer);
	    FREE(buffer);
	    result = rv;
	}
    } else {

	/*
	 * Push the data down the stream
	 *
	 * @@ Bug:  This decision ought to be made based on "encoding" rather than
	 * on content-type.  @@@ When we handle encoding.  The current method
	 * smells anyway.
	 */
	targetClass = *(stream->isa);	/* Copy pointers to procedures */
	rv = HTBzFileCopy(bzfp, stream);
	if (rv == -1 || rv == HT_INTERRUPTED) {
	    (*targetClass._abort) (stream, NULL);
	} else {
	    (*targetClass._free) (stream);
	}

	HTCloseBzFile(bzfp);
	if (rv == -1) {
	    result = HT_NO_DATA;
	} else if (rv == HT_INTERRUPTED || (rv > 0 && rv != HT_LOADED)) {
	    result = HT_PARTIAL_CONTENT;
	} else {
	    result = HT_LOADED;
	}
    }
    return result;
}
#endif /* USE_BZLIB */

/*	Converter stream: Network Telnet to internal character text
 *	-----------------------------------------------------------
 *
 *	The input is assumed to be in ASCII, with lines delimited
 *	by (13,10) pairs, These pairs are converted into (CR,LF)
 *	pairs in the local representation.  The (CR,LF) sequence
 *	when found is changed to a '\n' character, the internal
 *	C representation of a new line.
 */

static void NetToText_put_character(HTStream *me, int net_char)
{
    char c = (char) FROMASCII(net_char);

    if (me->had_cr) {
	if (c == LF) {
	    me->sink->isa->put_character(me->sink, '\n');	/* Newline */
	    me->had_cr = NO;
	    return;
	} else {
	    me->sink->isa->put_character(me->sink, CR);		/* leftover */
	}
    }
    me->had_cr = (BOOL) (c == CR);
    if (!me->had_cr)
	me->sink->isa->put_character(me->sink, c);	/* normal */
}

static void NetToText_put_string(HTStream *me, const char *s)
{
    const char *p;

    for (p = s; *p; p++)
	NetToText_put_character(me, *p);
}

static void NetToText_put_block(HTStream *me, const char *s, int l)
{
    const char *p;

    for (p = s; p < (s + l); p++)
	NetToText_put_character(me, *p);
}

static void NetToText_free(HTStream *me)
{
    (me->sink->isa->_free) (me->sink);	/* Close rest of pipe */
    FREE(me);
}

static void NetToText_abort(HTStream *me, HTError e)
{
    me->sink->isa->_abort(me->sink, e);		/* Abort rest of pipe */
    FREE(me);
}

/*	The class structure
*/
static HTStreamClass NetToTextClass =
{
    "NetToText",
    NetToText_free,
    NetToText_abort,
    NetToText_put_character,
    NetToText_put_string,
    NetToText_put_block
};

/*	The creation method
*/
HTStream *HTNetToText(HTStream *sink)
{
    HTStream *me = typecalloc(HTStream);

    if (me == NULL)
	outofmem(__FILE__, "NetToText");

    me->isa = &NetToTextClass;

    me->had_cr = NO;
    me->sink = sink;
    return me;
}

static HTStream HTBaseStreamInstance;	/* Made static */

/*
 *	ERROR STREAM
 *	------------
 *	There is only one error stream shared by anyone who wants a
 *	generic error returned from all stream methods.
 */
static void HTErrorStream_put_character(HTStream *me GCC_UNUSED, int c GCC_UNUSED)
{
    LYCancelDownload = TRUE;
}

static void HTErrorStream_put_string(HTStream *me GCC_UNUSED, const char *s)
{
    if (s && *s)
	LYCancelDownload = TRUE;
}

static void HTErrorStream_write(HTStream *me GCC_UNUSED, const char *s, int l)
{
    if (l && s)
	LYCancelDownload = TRUE;
}

static void HTErrorStream_free(HTStream *me GCC_UNUSED)
{
    return;
}

static void HTErrorStream_abort(HTStream *me GCC_UNUSED, HTError e GCC_UNUSED)
{
    return;
}

static const HTStreamClass HTErrorStreamClass =
{
    "ErrorStream",
    HTErrorStream_free,
    HTErrorStream_abort,
    HTErrorStream_put_character,
    HTErrorStream_put_string,
    HTErrorStream_write
};

HTStream *HTErrorStream(void)
{
    CTRACE((tfp, "ErrorStream. Created\n"));
    HTBaseStreamInstance.isa = &HTErrorStreamClass;	/* The rest is random */
    return &HTBaseStreamInstance;
}