about summary refs log tree commit diff stats
path: root/WWW/Library/Implementation/HTFormat.c
diff options
context:
space:
mode:
Diffstat (limited to 'WWW/Library/Implementation/HTFormat.c')
-rw-r--r--WWW/Library/Implementation/HTFormat.c836
1 files changed, 836 insertions, 0 deletions
diff --git a/WWW/Library/Implementation/HTFormat.c b/WWW/Library/Implementation/HTFormat.c
new file mode 100644
index 00000000..f25d36b3
--- /dev/null
+++ b/WWW/Library/Implementation/HTFormat.c
@@ -0,0 +1,836 @@
+
+/*		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.
+**
+*/
+
+
+#include "HTUtils.h"
+#include "tcp.h"
+
+/* Implements:
+*/
+#include "HTFormat.h"
+
+PUBLIC float HTMaxSecs = 1e10;		/* No effective limit */
+PUBLIC float HTMaxLength = 1e10;	/* No effective limit */
+PUBLIC long int HTMaxBytes  = 0;	/* No effective limit */
+
+PUBLIC int loading_length= -1;
+
+#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
+#endif
+
+
+#include "HTML.h"
+#include "HTMLDTD.h"
+#include "HText.h"
+#include "HTAlert.h"
+#include "HTList.h"
+#include "HTInit.h"
+#include "HTTCP.h"
+/*	Streams and structured streams which we use:
+*/
+#include "HTFWriter.h"
+#include "HTPlain.h"
+#include "SGML.h"
+#include "HTML.h"
+#include "HTMLGen.h"
+
+#include "LYexit.h"
+#include "LYLeaks.h"
+
+#define FREE(x) if (x) {free(x); x = NULL;}
+
+extern int HTCheckForInterrupt NOPARAMS;
+
+PUBLIC	BOOL HTOutputSource = NO;	/* Flag: shortcut parser to stdout */
+/* extern  BOOL interactive; LJM */
+
+#ifdef ORIGINAL
+struct _HTStream {
+      CONST HTStreamClass*	isa;
+      /* ... */
+};
+#endif
+
+/* this version used by the NetToText stream */
+struct _HTStream {
+	CONST HTStreamClass *	isa;
+	BOOL			had_cr;
+	HTStream * 		sink;
+};
+
+
+/*	Presentation methods
+**	--------------------
+*/
+
+PUBLIC  HTList * HTPresentations = NULL;
+PUBLIC  HTPresentation * default_presentation = NULL;
+
+/*
+ *	To free off the presentation list.
+ */
+PRIVATE void HTFreePresentations NOPARAMS;
+
+/*	Define a presentation system command for a content-type
+**	-------------------------------------------------------
+*/
+PUBLIC void HTSetPresentation ARGS6(
+	CONST char *,	representation,
+	CONST char *,	command,
+	float,		quality,
+	float,		secs, 
+	float,		secs_per_byte,
+	long int,	maxbytes)
+{
+    HTPresentation * pres = (HTPresentation *)malloc(sizeof(HTPresentation));
+    if (pres == NULL)
+        outofmem(__FILE__, "HTSetPresentation");
+    
+    pres->rep = HTAtom_for(representation);
+    pres->rep_out = WWW_PRESENT;		/* Fixed for now ... :-) */
+    pres->converter = HTSaveAndExecute;		/* Fixed for now ...     */
+    pres->quality = quality;
+    pres->secs = secs;
+    pres->secs_per_byte = secs_per_byte;
+    pres->maxbytes = maxbytes;
+    pres->command = NULL;
+    StrAllocCopy(pres->command, command);
+   
+    /*
+     *	Memory leak fixed.
+     *  05-28-94 Lynx 2-3-1 Garrett Arch Blythe
+     */ 
+    if (!HTPresentations)	{
+	HTPresentations = HTList_new();
+	atexit(HTFreePresentations);
+    }
+    
+    if (strcmp(representation, "*")==0) {
+	FREE(default_presentation);
+	default_presentation = pres;
+    } else {
+        HTList_addObject(HTPresentations, pres);
+    }
+}
+
+
+/*	Define a built-in function for a content-type
+**	---------------------------------------------
+*/
+PUBLIC void HTSetConversion ARGS7(
+	CONST char *,	representation_in,
+	CONST char *,	representation_out,
+	HTConverter*,	converter,
+	float,		quality,
+	float,		secs, 
+	float,		secs_per_byte,
+	long int,	maxbytes)
+{
+    HTPresentation * pres = (HTPresentation *)malloc(sizeof(HTPresentation));
+    if (pres == NULL)
+        outofmem(__FILE__, "HTSetConversion");
+    
+    pres->rep = HTAtom_for(representation_in);
+    pres->rep_out = HTAtom_for(representation_out);
+    pres->converter = converter;
+    pres->command = NULL;		/* Fixed */
+    pres->quality = quality;
+    pres->secs = secs;
+    pres->secs_per_byte = secs_per_byte;
+    pres->maxbytes = maxbytes;
+    pres->command = NULL;
+   
+    /*
+     *	Memory Leak fixed.
+     *	05-28-94 Lynx 2-3-1 Garrett Arch Blythe
+     */ 
+    if (!HTPresentations)	{
+	HTPresentations = HTList_new();
+	atexit(HTFreePresentations);
+    }
+    
+    HTList_addObject(HTPresentations, pres);
+}
+
+/*
+**	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
+*/
+PRIVATE void HTFreePresentations NOARGS
+{
+    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);
+    }
+    /*
+     *	Free the list itself.
+     */
+    HTList_delete(HTPresentations);
+    HTPresentations = NULL;
+}
+
+
+/*	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 */
+PRIVATE char input_buffer[INPUT_BUFFER_SIZE];
+PRIVATE char * input_pointer;
+PRIVATE char * input_limit;
+PRIVATE 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.
+*/
+PUBLIC void HTInitInput ARGS1 (int,file_number)
+{
+    input_file_number = file_number;
+    input_pointer = input_limit = input_buffer;
+}
+
+PUBLIC int interrupted_in_htgetcharacter = 0;
+PUBLIC char HTGetCharacter NOARGS
+{
+    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 (char)EOF;
+		if (status == HT_INTERRUPTED)
+                {
+                  if (TRACE)
+                    fprintf (stderr,
+			     "HTFormat: Interrupted in HTGetCharacter\n");
+                  interrupted_in_htgetcharacter = 1;
+                  return (char)EOF;
+                }
+		if (TRACE) fprintf(stderr,
+		    "HTFormat: File 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);
+}
+
+/*	Stream the data to an ouput file as binary
+*/
+PUBLIC int HTOutputBinary ARGS2( int, 		input,
+				  FILE *, 	output)
+{
+    do {
+	    int status = NETREAD(
+		    input, input_buffer, INPUT_BUFFER_SIZE);
+	    if (status <= 0) {
+		if (status == 0) return 0;
+		if (TRACE) fprintf(stderr,
+		    "HTFormat: File read error %d\n", status);
+		return 2;			/* Error */
+	    }
+	    fwrite(input_buffer, sizeof(char), status, output);
+    } while (YES);
+}
+
+/*  Match maintype to any MIME type starting with maintype,
+ *  for example:  image/gif should match image
+ */
+PRIVATE int half_match ARGS2(char *,trial_type, char *,target)
+{
+    char *cp=strchr(trial_type,'/');
+
+    /* if no '/' or no '*' */
+    if(!cp || *(cp+1) != '*')
+	return 0;
+
+    if(TRACE)
+	fprintf(stderr,"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;
+}
+
+
+/*		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.
+**
+*/
+PUBLIC HTStream * HTStreamStack ARGS4(
+	HTFormat,		rep_in,
+	HTFormat,		rep_out,
+	HTStream*,		sink,
+	HTParentAnchor*,	anchor)
+{
+    HTAtom * wildcard = HTAtom_for("*");
+
+    if (TRACE) fprintf(stderr,
+    	"HTFormat: Constructing stream stack for %s to %s\n",
+	HTAtom_name(rep_in),	
+	HTAtom_name(rep_out));
+		
+       /* don't return on WWW_SOURCE some people might like
+        * to make use of the source!!!!  LJM
+        */
+     /* if (rep_out == WWW_SOURCE ||
+       		rep_out == rep_in) return sink;  LJM */
+
+     if(rep_out == rep_in) return sink;
+
+	/* don't do anymore do it in the Lynx code at startup LJM */
+    /* if (!HTPresentations) HTFormatInit(); */	/* set up the list */
+    
+    {
+	int n = HTList_count(HTPresentations);
+	int i;
+	HTPresentation * pres, *match,
+			*strong_wildcard_match=0,
+			*weak_wildcard_match=0,
+			*last_default_match=0,
+			*strong_subtype_wildcard_match=0;
+
+	for(i=0; i<n; i++) {
+	    pres = (HTPresentation *)HTList_objectAt(HTPresentations, i);
+	    if (pres->rep == rep_in) {
+	        if (pres->rep_out == rep_out) {
+		    if(TRACE)
+			fprintf(stderr,"StreamStack: found exact match: %s\n",HTAtom_name(pres->rep));
+	    	    return (*pres->converter)(pres, anchor, sink);
+
+		} else if (pres->rep_out == wildcard) {
+		    if(!strong_wildcard_match)
+		        strong_wildcard_match = pres;
+		    /* otherwise use the first one */
+		    if(TRACE)
+			fprintf(stderr,"StreamStack: found strong wildcard match: %s\n",HTAtom_name(pres->rep));
+		}
+
+	    } else if(half_match(HTAtom_name(pres->rep),
+						HTAtom_name(rep_in))) {
+		
+	        if (pres->rep_out == rep_out) {
+		    if(!strong_subtype_wildcard_match)
+		       strong_subtype_wildcard_match = pres;
+		    /* otherwise use the first one */
+		    if(TRACE)
+			fprintf(stderr,"StreamStack: found strong subtype wildcard match: %s\n",HTAtom_name(pres->rep));
+		}
+	    }
+
+	    if (pres->rep == WWW_SOURCE) {
+		if(pres->rep_out == rep_out) {
+		    if(!weak_wildcard_match)
+		        weak_wildcard_match = pres;
+		    /* otherwise use the first one */
+		    if(TRACE)
+			fprintf(stderr,"StreamStack: found weak wildcard match: %s\n",HTAtom_name(pres->rep_out));
+
+		}
+		if(pres->rep_out == wildcard) {
+		    if(!last_default_match)
+		        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) {
+		HTPresentation temp;
+		temp = *match;			/* Specific instance */
+		temp.rep = rep_in;		/* yuk */
+		temp.rep_out = rep_out;		/* yuk */
+		if(TRACE)
+		    fprintf(stderr,"StreamStack: Using %s\n",HTAtom_name(temp.rep_out));
+		return (*match->converter)(&temp, anchor, sink);
+        }
+    }
+
+    return NULL;
+}
+	
+
+/*		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
+*/
+PUBLIC float HTStackValue ARGS4(
+	HTFormat,		rep_in,
+	HTFormat,		rep_out,
+	float,			initial_value,
+	long int,		length)
+{
+    HTAtom * wildcard = HTAtom_for("*");
+
+    if (TRACE) fprintf(stderr,
+    	"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;
+
+	/* don't do anymore do it in the Lynx code at startup LJM */
+    /* if (!HTPresentations) HTFormatInit(); */	/* set up the list */
+    
+    {
+	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 = initial_value * pres->quality;
+		if (HTMaxSecs != 0.0)
+		    value = value - (length*pres->secs_per_byte + pres->secs)
+			                 /HTMaxSecs;
+		return value;
+	    }
+	}
+    }
+    
+    return -1e30;		/* Really bad */
+
+}
+	
+
+/*	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 ie containing
+**   CRLF at the end of lines which need to be stripped to LF for unix
+**   when the format is textual.
+**
+*/
+
+PUBLIC int HTCopy ARGS3(
+	int,			file_number,
+	void*,			handle,
+	HTStream*,		sink)
+{
+    HTStreamClass targetClass;    
+    char line[256];
+    int bytes=0;
+    int rv = 0;
+    char * msg;
+
+    if (loading_length == -1)
+        msg = "Read %d bytes of data.";
+    else
+        /* We have a loading_length. */
+        msg = "Read %d of %d bytes of data.";
+
+    
+/*	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
+    */
+    for(;;) {
+        int status;
+	extern char LYCancelDownload;
+
+	if (LYCancelDownload) {
+	    LYCancelDownload = FALSE;
+	    (*targetClass._abort)(sink, NULL);
+	    rv = -1;
+	    goto finished;
+	}
+
+        if (HTCheckForInterrupt())
+          {
+            _HTProgress ("Data transfer interrupted.");
+            (*targetClass._abort)(sink, NULL);
+	    if(bytes)
+                rv = HT_INTERRUPTED;
+	    else
+		rv = -1;
+	    goto finished;
+          }
+
+
+        status = NETREAD(file_number, input_buffer, INPUT_BUFFER_SIZE);
+
+	if (status <= 0) {
+	    if (status == 0) 
+	      break;
+	    else if (status == HT_INTERRUPTED)
+              {
+                _HTProgress ("Data transfer interrupted.");
+                (*targetClass._abort)(sink, NULL);
+	        if(bytes)
+                    rv = HT_INTERRUPTED;
+	        else
+		    rv = -1;
+	        goto finished;
+              }
+	    else if (SOCKET_ERRNO == ENOTCONN || SOCKET_ERRNO == ECONNRESET 
+						   || SOCKET_ERRNO == EPIPE)
+              {
+                /* Arrrrgh, HTTP 0/1 compability problem, maybe. */
+		rv = -2;
+	        goto finished;
+              }
+            break;
+	}
+
+#ifdef NOT_ASCII
+	{
+	    char * p;
+	    for(p = input_buffer; p < input_buffer+status; p++) {
+		*p = FROMASCII(*p);
+	    }
+	}
+#endif
+
+	(*targetClass.put_block)(sink, input_buffer, status);
+
+	bytes += status;
+        sprintf(line, msg, bytes, loading_length);
+        HTProgress(line);
+
+    } /* next bufferload */
+
+    _HTProgress("Data transfer complete");
+    NETCLOSE(file_number);
+    rv = HT_LOADED;
+
+finished:
+    loading_length = -1;
+    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.
+**
+**
+*/
+PUBLIC void HTFileCopy ARGS2(
+	FILE *,			fp,
+	HTStream*,		sink)
+{
+    HTStreamClass targetClass;    
+    
+/*	Push the data down the stream
+**
+*/
+    targetClass = *(sink->isa);	/* Copy pointers to procedures */
+    
+    /*	Push binary from socket down sink
+    */
+    for(;;) {
+	int status = fread(
+	       input_buffer, 1, INPUT_BUFFER_SIZE, fp);
+	if (status == 0) { /* EOF or error */
+	    if (ferror(fp) == 0) break;
+	    if (TRACE) fprintf(stderr,
+		"HTFormat: Read error, read returns %d\n", ferror(fp));
+	    break;
+	}
+	(*targetClass.put_block)(sink, input_buffer, status);
+    } /* next bufferload */
+	
+}
+
+
+
+
+/*	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.
+**
+*/
+PUBLIC void HTCopyNoCR ARGS2(
+	int,			file_number,
+	HTStream*,		sink)
+{
+    HTStreamClass targetClass;    
+    
+/*	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(;;) {
+	char character;
+	character = HTGetCharacter();
+	if (character == (char)EOF) break;
+	(*targetClass.put_character)(sink, 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.
+**
+*/
+PUBLIC int HTParseSocket ARGS5(
+	HTFormat,		rep_in,
+	HTFormat,		format_out,
+	HTParentAnchor *,	anchor,
+	int,			file_number,
+	HTStream*,		sink)
+{
+    HTStream * stream;
+    HTStreamClass targetClass;
+    int rv;
+    extern char LYCancelDownload;
+
+    stream = HTStreamStack(rep_in,
+			format_out,
+	 		sink , anchor);
+    
+    if (!stream) {
+        char buffer[1024];	/* @@@@@@@@ */
+        if (LYCancelDownload) {
+	    LYCancelDownload = FALSE;
+	    return -1;
+	}
+	sprintf(buffer, "Sorry, can't convert from %s to %s.",
+	        HTAtom_name(rep_in), HTAtom_name(format_out));
+	if (TRACE) fprintf(stderr, "HTFormat: %s\n", buffer);
+        return HTLoadError(sink, 501, buffer); /* returns -501 */
+    }
+    
+/*
+**	Push the data, don't worry about CRLF we can strip them later.
+*/
+    targetClass = *(stream->isa);	/* Copy pointers to procedures */
+    rv = HTCopy(file_number, NULL, stream);
+    if (rv != -1 && rv != HT_INTERRUPTED)
+        (*targetClass._free)(stream);
+    
+    return rv; /* 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.
+**
+*/
+PUBLIC int HTParseFile ARGS5(
+	HTFormat,		rep_in,
+	HTFormat,		format_out,
+	HTParentAnchor *,	anchor,
+	FILE *,			fp,
+	HTStream*,		sink)
+{
+    HTStream * stream;
+    HTStreamClass targetClass;    
+
+    stream = HTStreamStack(rep_in,
+			format_out,
+	 		sink , anchor);
+    
+    if (!stream) {
+        char buffer[1024];	/* @@@@@@@@ */
+	extern char LYCancelDownload;
+        if (LYCancelDownload) {
+	    LYCancelDownload = FALSE;
+	    return -1;
+	}
+	sprintf(buffer, "Sorry, can't convert from %s to %s.",
+		HTAtom_name(rep_in), HTAtom_name(format_out));
+	if (TRACE) fprintf(stderr, "HTFormat(in HTParseFile): %s\n", buffer);
+        return HTLoadError(sink, 501, buffer);
+    }
+    
+/*	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 */
+    HTFileCopy(fp, stream);
+    (*targetClass._free)(stream);
+    
+    return HT_LOADED;
+}
+
+
+/*	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.
+*/
+
+
+PRIVATE void NetToText_put_character ARGS2(HTStream *, me, char, net_char)
+{
+    char c = 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 = (c==CR);
+    if (!me->had_cr)
+	me->sink->isa->put_character(me->sink, c);		/* normal */
+}
+
+PRIVATE void NetToText_put_string ARGS2(HTStream *, me, CONST char *, s)
+{
+    CONST char * p;
+
+    for (p=s; *p; p++)
+        NetToText_put_character(me, *p);
+}
+
+PRIVATE void NetToText_put_block ARGS3(HTStream *, me, CONST char*, s, int, l)
+{
+    CONST char * p;
+    for(p=s; p<(s+l); p++) NetToText_put_character(me, *p);
+}
+
+PRIVATE void NetToText_free ARGS1(HTStream *, me)
+{
+    (me->sink->isa->_free)(me->sink);		/* Close rest of pipe */
+    FREE(me);
+}
+
+PRIVATE void NetToText_abort ARGS2(HTStream *, me, HTError, e)
+{
+    me->sink->isa->_abort(me->sink,e);		/* Abort rest of pipe */
+    FREE(me);
+}
+
+/*	The class structure
+*/
+PRIVATE HTStreamClass NetToTextClass = {
+    "NetToText",
+    NetToText_free,
+    NetToText_abort,
+    NetToText_put_character,
+    NetToText_put_string,
+    NetToText_put_block
+};
+
+/*	The creation method
+*/
+PUBLIC HTStream * HTNetToText ARGS1(HTStream *, sink)
+{
+    HTStream* me = (HTStream*)malloc(sizeof(*me));
+    if (me == NULL)
+        outofmem(__FILE__, "NetToText");
+    me->isa = &NetToTextClass;
+    
+    me->had_cr = NO;
+    me->sink = sink;
+    return me;
+}
+