Hello.

So i am doing this, onto v2-9-2b.
Please some useless headache rants first, thank you..

  , and i hate autotools, it took me two hours alone to get this
  going vs fancy curses and lynx defining getbegx() and

  -    SCREEN *oldscreen = LYscreen;
  +    SCREEN *oldscreen = (SCREEN*)LYscreen;

  also delete_screen() is unknown.
  I had to edit configure because i dunno because of
  $ac_cv_sys_file_offset_bits being empty in

    configure:      
CPPFLAGS="${CPPFLAGS}-D_FILE_OFFSET_BITS=$ac_cv_sys_file_offset_bits"

    In file included from conftest.c:27:
    In file included from /usr/include/limits.h:26:
    In file included from /usr/include/bits/libc-header-start.h:33:
    /usr/include/features.h:390:52: error: invalid token at start of a 
preprocessor expression
      390 | #if defined _FILE_OFFSET_BITS && _FILE_OFFSET_BITS == 64
          |                                                    ^

  but again, i have no idea.  Sigh.  Shall i ever do anything lynx
  again then only on a release ball, really.  Too late again.  Hm.
  "My lynx", fwiw, is

    ./configure --disable-color-style --disable-config-info \
      --disable-dired --disable-echo --disable-gopher \
      --disable-included-msgs --disable-menu-options \
      --disable-prettysrc --disable-scrollbar \
      --disable-session-cache --disable-sessions \
      --disable-trace --enable-default-colors --enable-gzip-help \
      --enable-ipv6 --enable-nested-tables \
      --enable-underlines --enable-widec --with-ssl \
      --with-zlib --with-zstd

:-)

Ok, so i did that *i think*.  It needs more careful testing.
It must be said that i think the Brotli addition was not truly
tested without other *Z*, i do not think this will do then -- some
fixes for that are included in the patch below.
I am an autotools idiot.  Btw i see

  ...
  checking for zError... yes
  checking if you want to use brotli decompression... yes
  configure: WARNING: Cannot find brotlidec library
  checking if you want to use zstd decompression... yes
  ...
  checking for bzip2... /usr/bin/bzip2
  checking for brotli... no
  checking for brotli... no
  checking for zstd... /usr/bin/zstd
  ...

I am a day late, because i lost several hours due to myriads of

  Sending HTTP request.
  HTTP request sent; waiting for response.
  HTTP/1.0 200 OK
  Data transfer complete

  Alert!: Error uncompressing temporary file!

  lynx: Start file could not be found or is not text/html or text/plain
        Exiting...

errors that i did not understand.  By sheer despair i then changed
in HTFWriter_free() the first

  #ifdef USE_ZLIB

to

  #ifdef xUSE_ZLIB

and convinced myself that the code path doing content-encoding
decompression via gzip(1) is broken, thus "also", and thus i could
not help that code path for now:

  ACCEPT ENCODING IS gzip, bzip2, zstd                                          
                 (
  CODING IS zstd                                                                
                 | CODING IS gzip
  CONTEN-TYPE_IST text/html                                                     
                 (
  NOW THI SIS NOT UTST: text/html                                               
                 (
  CODING IS zstd                                                                
                 | CODING IS gzip
   FOUND AND SET ZSTD /usr/bin/zstd --rm -d %s                                  
                 |  FOUND AND SET GZIP /bin/gzip -d --no-name %s
   AFTER FIRST IF                                                               
                 (
  HTCOMPRESSED ANCHORT CT is text/html                                          
                 (
   TEMP is .html.zst                                                            
                 |  TEMP is .html.gz
  PROGRAM SET RECEIVING COMPRESSED /tmp/lynxXXXX8vtgxq/L18273-1TMP.html.zst 
.html.zst            | PROGRAM SET RECEIVING COMPRESSED 
/tmp/lynxXXXXOFtvdP/L18191-1TMP.html.gz .html.gz
  RETURN HTCOMRPESSED() /tmp/lynxXXXX8vtgxq/L18273-1TMP.html.zst                
                 | RETURN HTCOMRPESSED() /tmp/lynxXXXXOFtvdP/L18191-1TMP.html.gz
  WRITER_FREE CT zstd                                                           
                 | WRITER_FREE CT gzip
  COMPRESSION FORMET www/compressed check=2                                     
                 (
  ISCF GZIP                                                                     
                 (
  COMPRESSION FORMET www/compressed check=5                                     
                 (
  ISCF BROTLI                                                                   
                 (
  COMPRESSION FORMET www/compressed check=6                                     
                 (
  ISCF ZSTD                                                                     
                 (
       DECOMPRESS, ELSE www/compressed                                          
                 (
     SKIP_LOADFILE=0 viewer=                                                    
                 (
        ==YEAH ZSTD                                                             
                 |       ==YEAH GZUP
  PATH REMOVE ZSTD IS /tmp/lynxXXXX8vtgxq/L18273-1TMP.html                      
                 | PATH REMOVE GZIP IS /tmp/lynxXXXXOFtvdP/L18191-1TMP.html
       !use_zread                                                               
                 (
       FOUND <> uncompress failed                                               
                 (

(In my experience the "PATH REMOVE" file *is already* plain HTML,
ie, somehow, magically, uncompressed.  I have the debug soiled
code around still..)

Ciao!

--steffen
|
|Der Kragenbaer,                The moon bear,
|der holt sich munter           he cheerfully and one by one
|einen nach dem anderen runter  wa.ks himself off
|(By Robert Gernhardt)
From 6fa4291903326c823f00c0cb9e98223114b7ebcd Mon Sep 17 00:00:00 2001
Message-ID: <6fa4291903326c823f00c0cb9e98223114b7ebcd.1720043570.git.stef...@sdaoden.eu>
From: Steffen Nurpmeso <stef...@sdaoden.eu>
Date: Wed, 3 Jul 2024 23:52:02 +0200
Subject: [PATCH] zstd encoding support

---
 INSTALLATION                          |  10 +-
 PACKAGE/lynx.spec                     |   5 +-
 WWW/Library/Implementation/HTFTP.c    |   3 +
 WWW/Library/Implementation/HTFile.c   |  72 +++++++++-
 WWW/Library/Implementation/HTFile.h   |   2 +
 WWW/Library/Implementation/HTFormat.c | 181 +++++++++++++++++++++++++-
 WWW/Library/Implementation/HTFormat.h |  19 ++-
 WWW/Library/Implementation/HTTP.c     |   3 +
 aclocal.m4                            |  21 +++
 config.hin                            |   2 +
 configure.in                          |  14 ++
 lynx.cfg                              |   6 +
 src/HTFWriter.c                       | 157 ++++++++++++++++++++++
 src/HTInit.c                          |   2 +
 src/LYReadCFG.c                       |   1 +
 src/LYrcFile.c                        |   3 +
 src/LYrcFile.h                        |   1 +
 17 files changed, 496 insertions(+), 6 deletions(-)

diff --git a/INSTALLATION b/INSTALLATION
index 68acdbbb4e..ca590f5dc0 100644
--- a/INSTALLATION
+++ b/INSTALLATION
@@ -300,7 +300,7 @@ II. Compile instructions -- UNIX
 	
 	Affected commands include
 		chmod compress cp gzip install mkdir mv rm tar touch gunzip
-		unzip brotli bzip2 uudecode zcat zip telnet tn3270 rlogin
+		unzip brotli bzip2 uudecode zcat zip zstd telnet tn3270 rlogin
 	(Not all commands are used on all systems or in all configurations.)
 
 	This option makes Lynx simpler to install, but potentially less secure,
@@ -787,6 +787,14 @@ II. Compile instructions -- UNIX
 	or one level above.  In either case, the corresponding header files
 	are assumed to be in the parallel "include" directory.
 
+  --with-zstd[=XXX]			(define USE_ZSTD)
+	Use zstd decompression.
+
+	The optional value XXX specifies the directory in which the library
+	can be found, and may be either the path of the "lib" directory,
+	or one level above.  In either case, the corresponding header files
+	are assumed to be in the parallel "include" directory.
+
 1d. Environment variables
     The configure script looks for programs and libraries in known/standard
     locations.  You can override the behavior of the script by presetting
diff --git a/PACKAGE/lynx.spec b/PACKAGE/lynx.spec
index 858171dbe7..da93c4cb8c 100644
--- a/PACKAGE/lynx.spec
+++ b/PACKAGE/lynx.spec
@@ -22,7 +22,7 @@ BuildRequires: libidn-devel
 # BuildRequires: libopenssl-1_1-devel, or
 # BuildRequires: libopenssl-3-devel                   
 
-Requires: brotli, gzip, bzip2, tar, zip, unzip
+Requires: brotli, gzip, bzip2, tar, zip, unzip, zstd
 
 %description
 Lynx is a fully-featured World Wide Web (WWW) client for users running
@@ -102,6 +102,9 @@ strip $RPM_BUILD_ROOT%{_bindir}/%{name}
 
 %changelog
 
+* Sat Jul 03 2024 Thomas E. Dickey
+- add zstd compression
+
 * Thu Mar 14 2024 Thomas E. Dickey
 - trim redundant options
 
diff --git a/WWW/Library/Implementation/HTFTP.c b/WWW/Library/Implementation/HTFTP.c
index a08701cfb2..ee854ff78b 100644
--- a/WWW/Library/Implementation/HTFTP.c
+++ b/WWW/Library/Implementation/HTFTP.c
@@ -4095,6 +4095,9 @@ int HTFTPLoad(const char *name,
 	    case cftBrotli:
 		StrAllocCopy(anchor->content_encoding, "x-brotli");
 		break;
+	    case cftZstd:
+		StrAllocCopy(anchor->content_encoding, "zstd");
+		break;
 	    case cftNone:
 		break;
 	    }
diff --git a/WWW/Library/Implementation/HTFile.c b/WWW/Library/Implementation/HTFile.c
index 9b6c54f674..d5db0bd9e9 100644
--- a/WWW/Library/Implementation/HTFile.c
+++ b/WWW/Library/Implementation/HTFile.c
@@ -1283,6 +1283,11 @@ CompressFileType HTCompressFileType(const char *filename,
 		   && StrChr(dots, ftype[-4]) != 0) {
 	    result = cftBzip2;
 	    ftype -= 4;
+	} else if ((len > 4)
+		   && !strcasecomp((ftype - 3), "zst")
+		   && StrChr(dots, ftype[-4]) != 0) {
+	    result = cftZstd;
+	    ftype -= 4;
 	} else if ((len > 3)
 		   && !strcasecomp((ftype - 2), "gz")
 		   && StrChr(dots, ftype[-3]) != 0) {
@@ -1335,6 +1340,9 @@ const char *HTCompressTypeToSuffix(CompressFileType method)
     case cftBrotli:
 	result = ".br";
 	break;
+    case cftZstd:
+	result = ".zst";
+	break;
     }
     return result;
 }
@@ -1366,6 +1374,9 @@ const char *HTCompressTypeToEncoding(CompressFileType method)
     case cftBrotli:
 	result = "brotli";
 	break;
+    case cftZstd:
+	result = "zstd";
+	break;
     }
     return result;
 }
@@ -1377,6 +1388,7 @@ const char *HTCompressTypeToEncoding(CompressFileType method)
  *	compress
  *	deflate
  * as well as "identity" (but that does nothing).
+ * RFC 8878 gives zstd.
  */
 CompressFileType HTEncodingToCompressType(const char *coding)
 {
@@ -1400,6 +1412,8 @@ CompressFileType HTEncodingToCompressType(const char *coding)
 	       !strcasecomp(coding, "x-br") ||
 	       !strcasecomp(coding, "brotli")) {	/* expected */
 	result = cftBrotli;
+    } else if (!strcasecomp(coding, "zstd")) {
+	result = cftZstd;
     }
     return result;
 }
@@ -1426,6 +1440,8 @@ CompressFileType HTContentTypeToCompressType(const char *ct)
 	       !strncasecomp(ct, "application/x-br", 16) ||
 	       !strncasecomp(ct, "application/brotli", 18)) {
 	method = cftBrotli;
+    } else if (!strncasecomp(ct, "application/zstd", 16)) {
+	method = cftZstd;
     }
     return method;
 }
@@ -1459,6 +1475,9 @@ BOOL IsCompressionFormat(HTAtom *format, CompressFileType check)
 		  format == HTAtom_for("application/x-br") ||
 		  format == HTAtom_for("application/brotli"));
 	break;
+    case cftZstd:
+	result = (format == HTAtom_for("application/zstd"));
+	break;
     case cftNone:
 	break;
     }
@@ -2498,7 +2517,11 @@ static int decompressAndParse(HTParentAnchor *anchor,
 #ifdef USE_BROTLI
     FILE *brfp = 0;
 #endif /* USE_BROTLI */
-#if defined(USE_ZLIB) || defined(USE_BZLIB)
+#ifdef USE_ZSTD
+    FILE *zfp = 0;
+#endif
+#if defined(USE_ZLIB) || defined(USE_BZLIB) ||\
+	defined(USR_BROTLI) || defined(USE_ZSTD)
     CompressFileType internal_decompress = cftNone;
     BOOL failed_decompress = NO;
 #endif
@@ -2556,7 +2579,8 @@ static int decompressAndParse(HTParentAnchor *anchor,
 	     * this is a compressed file, no need to look at the filename
 	     * again.  - kw
 	     */
-#if defined(USE_ZLIB) || defined(USE_BZLIB)
+#if defined(USE_ZLIB) || defined(USE_BZLIB) ||\
+		defined(USE_BROTLI) || defined(USE_ZSTD)
 	    CompressFileType method = HTEncodingToCompressType(HTAtom_name(myEncoding));
 #endif
 
@@ -2607,6 +2631,16 @@ static int decompressAndParse(HTParentAnchor *anchor,
 		internal_decompress = cftBrotli;
 	    } else
 #endif /* USE_BROTLI */
+#ifdef USE_ZSTD
+	    if (isDOWNLOAD(cftZstd)) {
+		fclose(fp);
+		fp = 0;
+		zfp = fopen(localname, BIN_R);
+		CTRACE((tfp, "HTLoadFile: fopen of `%s' gives %p\n",
+			localname, (void *) zfp));
+		internal_decompress = cftZstd;
+	    } else
+#endif /* USE_ZSTD */
 	    {
 		StrAllocCopy(anchor->content_type, format->name);
 		StrAllocCopy(anchor->content_encoding, HTAtom_name(myEncoding));
@@ -2701,6 +2735,21 @@ static int decompressAndParse(HTParentAnchor *anchor,
 		format = HTAtom_for("www/compressed");
 #endif /* USE_BROTLI */
 		break;
+	    case cftZstd:
+		StrAllocCopy(anchor->content_encoding, "zstd");
+#ifdef USE_ZSTD
+		if (strcmp(format_out->name, STR_DOWNLOAD) != 0) {
+		    fclose(fp);
+		    fp = 0;
+		    zfp = fopen(localname, BIN_R);
+		    CTRACE((tfp, "HTLoadFile: fopen of `%s' gives %p\n",
+			    localname, (void *) zfp));
+		    internal_decompress = cftZstd;
+		}
+#else /* USE_ZSTD */
+		format = HTAtom_for("www/compressed");
+#endif /* USE_ZSTD */
+		break;
 	    case cftNone:
 		break;
 	    }
@@ -2726,6 +2775,11 @@ static int decompressAndParse(HTParentAnchor *anchor,
 	    case cftBrotli:
 		failed_decompress = (BOOLEAN) (brfp == NULL);
 		break;
+#endif
+#ifdef USE_ZSTD
+	    case cftZstd:
+		failed_decompress = (BOOLEAN) (zfp == NULL);
+		break;
 #endif
 	    default:
 		failed_decompress = YES;
@@ -2758,6 +2812,12 @@ static int decompressAndParse(HTParentAnchor *anchor,
 		if (sugfname && *sugfname)
 		    StrAllocCopy(anchor->SugFname, sugfname);
 		FREE(sugfname);
+#ifdef USE_ZSTD
+		if (zfp)
+		    *statusp = HTParseZstdFile(format, format_out,
+					     anchor,
+					     zfp, sink);
+#endif
 #ifdef USE_BROTLI
 		if (brfp)
 		    *statusp = HTParseBrFile(format, format_out,
@@ -3055,6 +3115,9 @@ int HTLoadFile(const char *addr,
 			case cftBrotli:
 			    atomname = "application/x-brotli";
 			    break;
+			case cftZstd:
+			    atomname = "application/zstd";
+			    break;
 			case cftNone:
 			    break;
 			}
@@ -3356,6 +3419,11 @@ void HTInitProgramPaths(BOOL init)
 
     for (n = (int) ppUnknown + 1; n < (int) pp_Last; ++n) {
 	switch (code = (ProgramPaths) n) {
+#ifdef ZSTD_PATH
+	case ppZSTD:
+	    path = ZSTD_PATH;
+	    break;
+#endif
 #ifdef BROTLI_PATH
 	case ppBROTLI:
 	    path = BROTLI_PATH;
diff --git a/WWW/Library/Implementation/HTFile.h b/WWW/Library/Implementation/HTFile.h
index 9340963773..652f065b16 100644
--- a/WWW/Library/Implementation/HTFile.h
+++ b/WWW/Library/Implementation/HTFile.h
@@ -214,6 +214,7 @@ extern "C" {
 	,cftBzip2
 	,cftDeflate
 	,cftBrotli
+	,cftZstd
     } CompressFileType;
 
 /*
@@ -336,6 +337,7 @@ extern "C" {
 	,ppUUDECODE
 	,ppZCAT
 	,ppZIP
+	,ppZSTD
 	,pp_Last
     } ProgramPaths;
 
diff --git a/WWW/Library/Implementation/HTFormat.c b/WWW/Library/Implementation/HTFormat.c
index ddb981cee3..f6780e43f9 100644
--- a/WWW/Library/Implementation/HTFormat.c
+++ b/WWW/Library/Implementation/HTFormat.c
@@ -61,6 +61,10 @@ static float HTMaxSecs = 1e10;	/* No effective limit */
 #include <brotli/decode.h>
 #endif
 
+#ifdef USE_ZSTD
+# include <zstd.h>
+#endif
+
 BOOL HTOutputSource = NO;	/* Flag: shortcut parser to stdout */
 
 /* this version used by the NetToText stream */
@@ -1481,7 +1485,108 @@ static int HTBrFileCopy(FILE *brfp, HTStream *sink)
     return rv;
 #undef THIS_FUNC
 }
-#endif /* USE_BZLIB */
+#endif /* USE_BROTLI */
+
+#ifdef USE_ZSTD
+/*	Push data from a zstd 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:
+ *		      ZFILE (zfp) assumed open (should have zstd 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		zfp still open, target stream still valid.
+ */
+static int HTZstdFileCopy(FILE *zfp, HTStream *sink)
+{
+	off_t bytes;
+	HTStreamClass targetClass;
+	ZSTD_DStream *izsp;
+	void *ibp, *obp;
+	size_t ibl, obl;
+	int rv;
+
+	rv = -1;
+
+	ibl = ZSTD_DStreamInSize();
+	ibp = malloc(ibl);
+	if (ibp == NULL)
+		goto jout0;
+
+	obl = ZSTD_DStreamOutSize();
+	obp = malloc(obl);
+	if (obp == NULL)
+		goto jout1;
+
+	if ((izsp = ZSTD_createDStream()) == NULL)
+		goto jout2;
+	ZSTD_initDStream(izsp);
+
+	/* Push the data down the stream */
+	targetClass = *(sink->isa);	/* Copy pointers to procedures */
+
+	/* read and inflate file, and push binary down sink */
+	HTReadProgress(bytes = 0, (off_t) 0);
+
+	for (;;) {
+		ZSTD_outBuffer ob;
+		ZSTD_inBuffer ib;
+		size_t r, w;
+
+		r = fread(ibp, 1, ibl, zfp);
+		if (r == 0) {
+			if (feof(zfp))
+				rv = HT_LOADED;
+			else if (bytes > 0)
+				rv = HT_PARTIAL_CONTENT;
+			break;
+		}
+
+		ib.src = ibp;
+		ib.size = r;
+		ib.pos = 0;
+		ob.dst = obp;
+		ob.size = obl;
+		ob.pos = 0;
+
+		r = ZSTD_decompressStream(izsp, &ob, &ib);
+		if (ZSTD_isError(r)) {
+			if (bytes > 0)
+				rv = HT_PARTIAL_CONTENT;
+			break;
+		}
+
+		(*targetClass.put_block) (sink, ob.dst, ob.pos);
+		bytes += ob.pos;
+		HTReadProgress(bytes, (off_t) -1);
+		HTDisplayPartial();
+
+		if (HTCheckForInterrupt()) {
+			_HTProgress(TRANSFER_INTERRUPTED);
+			rv = HT_INTERRUPTED;
+			break;
+		}
+	}	/* next bufferload */
+
+	ZSTD_freeDStream(izsp);
+jout2:	free(obp);
+jout1:	free(ibp);
+jout0:
+	HTFinishDisplayPartial();
+	return rv;
+}
+#endif /* USE_ZSTD */
 
 /*	Push data from a socket down a stream STRIPPING CR
  *	--------------------------------------------------
@@ -2048,6 +2153,80 @@ int HTParseBrFile(HTFormat rep_in,
 }
 #endif /* USE_BROTLI */
 
+#ifdef USE_ZSTD
+/*	HTParseZstdFile
+ *
+ *  State of file and target stream on entry:
+ *			FILE* (zfp) 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		zfp closed; target freed, aborted, or NULL.
+ */
+int HTParseZstdFile(HTFormat rep_in,
+		  HTFormat format_out,
+		  HTParentAnchor *anchor,
+		  FILE *zfp,
+		  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(zfp);
+		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 HTParseZstdFile): %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 = HTZstdFileCopy(zfp, stream);
+		if (rv == -1 || rv == HT_INTERRUPTED) {
+		    (*targetClass._abort) (stream, NULL);
+		} else {
+		    (*targetClass._free) (stream);
+		}
+
+		fclose(zfp);
+		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_ZSTD */
+
 /*	Converter stream: Network Telnet to internal character text
  *	-----------------------------------------------------------
  *
diff --git a/WWW/Library/Implementation/HTFormat.h b/WWW/Library/Implementation/HTFormat.h
index 835daef3f0..6015748791 100644
--- a/WWW/Library/Implementation/HTFormat.h
+++ b/WWW/Library/Implementation/HTFormat.h
@@ -238,11 +238,13 @@ The HTPresentation and HTConverter types
 	,encodingCOMPRESS = 4
 	,encodingBZIP2 = 8
 	,encodingBROTLI = 16
+	,encodingZSTD = 32
 	,encodingALL = (encodingGZIP
 			+ encodingDEFLATE
 			+ encodingCOMPRESS
 			+ encodingBZIP2
-			+ encodingBROTLI)
+			+ encodingBROTLI
+			+ encodingZSTD)
     } AcceptEncoding;
 
 /*
@@ -549,6 +551,21 @@ HTParseBrFile: Parse a brotli'ed File through a file pointer
 
 #endif				/* USE_BROTLI */
 
+#ifdef USE_ZSTD
+/*
+HTParseZstdFile: Parse a zstd'ed File through a file pointer
+
+   This routine is called by protocols modules to load an object.  uses
+   HTStreamStack and HTZstdFileCopy.  Returns HT_LOADED if successful, can also
+   return HT_PARTIAL_CONTENT, HT_NO_DATA, or other <0 for failure.
+ */
+    extern int HTParseZstdFile(HTFormat format_in,
+			       HTFormat format_out,
+			       HTParentAnchor *anchor,
+			       FILE *brfp,
+			       HTStream *sink);
+#endif				/* USE_ZSTD */
+
 /*
 
 HTNetToText: Convert Net ASCII to local representation
diff --git a/WWW/Library/Implementation/HTTP.c b/WWW/Library/Implementation/HTTP.c
index c47d446b71..f53c4c3fcb 100644
--- a/WWW/Library/Implementation/HTTP.c
+++ b/WWW/Library/Implementation/HTTP.c
@@ -713,6 +713,9 @@ static BOOL acceptEncoding(int code)
 	case encodingBROTLI:
 	    program = HTGetProgramPath(ppBROTLI);
 	    break;
+	case encodingZSTD:
+	    program = HTGetProgramPath(ppZSTD);
+	    break;
 	default:
 	    break;
 	}
diff --git a/aclocal.m4 b/aclocal.m4
index 471d935931..617ad11d6a 100644
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -7161,6 +7161,27 @@ AC_DEFUN([CF_WITH_BZLIB],[
 ],bz2,,,bzlib)
 ])dnl
 dnl ---------------------------------------------------------------------------
+dnl CF_WITH_ZSTD version: 1 updated: 2024/06/29 21:30:00
+dnl --------------
+dnl Check for Zstd decoder library
+dnl
+dnl $1 = optional path for headers/library
+AC_DEFUN([CF_WITH_ZSTD],[
+  CF_ADD_OPTIONAL_PATH($1, [zstd library])
+
+  CF_FIND_LINKAGE([
+#include <zstd.h>
+],[
+
+    ZSTD_DStream *izsp = ZSTD_createDStream();
+    ZSTD_inBuffer ib;
+    ZSTD_outBuffer ob;
+    ZSTD_initDStream(izsp);
+    ZSTD_decompressStream(izsp, &ob, &ib);
+    ZSTD_freeDStream(izsp);
+],zstd,,,zstd)
+])dnl
+dnl ---------------------------------------------------------------------------
 dnl CF_WITH_CURSES_DIR version: 4 updated: 2021/01/02 19:22:58
 dnl ------------------
 dnl Wrapper for AC_ARG_WITH to specify directory under which to look for curses
diff --git a/config.hin b/config.hin
index 764636144a..fbad64e492 100644
--- a/config.hin
+++ b/config.hin
@@ -293,6 +293,7 @@
 #undef USE_SYSV_UTMP		/* CF_UTMP */
 #undef USE_X509_SUPPORT		/* CF_GNUTLS, CF_SSL */
 #undef USE_ZLIB			/* AC_ARG_WITH(zlib) */
+#undef USE_ZSTD			/* AC_ARG_WITH(zstd) */
 #undef UTF8			/* CF_SLANG_CPPFLAGS */
 #undef UTMPX_FOR_UTMP		/* use <utmpx.h> since <utmp.h> not found */
 #undef UUDECODE_PATH		/* CF_PATH_PROG(uudecode) */
@@ -302,6 +303,7 @@
 #undef XCURSES			/* CF_PDCURSES_X11 */
 #undef ZCAT_PATH		/* CF_PATH_PROG(zcat) */
 #undef ZIP_PATH			/* CF_PATH_PROG(zip) */
+#undef ZSTD_PATH		/* CF_PATH_PROG(zstd) */
 #undef _WINDOWS_NSL		/* CF_ARG_ENABLE(nsl-fork) */
 #undef inline			/* AC_C_INLINE */
 #undef intptr_t			/* AC_CHECK_TYPE(intptr_t,...) */
diff --git a/configure.in b/configure.in
index 475c544e59..832fa8b814 100644
--- a/configure.in
+++ b/configure.in
@@ -1326,6 +1326,19 @@ if test ".$use_brotli" != ".no" ; then
 	test "x$cf_cv_find_linkage_brotlidec" = "xyes" && AC_DEFINE(USE_BROTLI,1,[Define to 1 if you want to use libbrotli decompression])
 fi
 
+dnl --------------------------------------------------------------------------
+AC_MSG_CHECKING(if you want to use zstd decompression)
+AC_ARG_WITH(zstd,
+[  --with-zstd             use zstd decompression],
+	[use_zstd=$withval],
+	[use_zstd=yes])
+AC_MSG_RESULT($use_zstd)
+
+if test ".$use_zstd" != ".no" ; then
+	CF_WITH_ZSTD($use_zstd)
+	test "x$cf_cv_find_linkage_zstd" = "xyes" && AC_DEFINE(USE_ZSTD,1,[Define to 1 if you want to use libzstd decompression])
+fi
+
 dnl --------------------------------------------------------------------------
 CF_HELP_MESSAGE(
 Other Network Services:)
@@ -1474,6 +1487,7 @@ CF_PATH_PROG(UNCOMPRESS,gunzip)
 CF_PATH_PROG(UNZIP,	unzip)
 CF_PATH_PROG(BZIP2,	bzip2)
 CF_PATH_PROG(BROTLI,	brotli)
+CF_PATH_PROG(ZSTD,	zstd)
 
 CF_PATH_PROG(TAR,	tar, pax gtar gnutar bsdtar star)
 CF_TAR_OPTIONS($TAR)
diff --git a/lynx.cfg b/lynx.cfg
index aa9ef975c1..18ad988f99 100644
--- a/lynx.cfg
+++ b/lynx.cfg
@@ -2262,6 +2262,7 @@ MINIMAL_COMMENTS:TRUE
 #SUFFIX:.Z::compress
 #SUFFIX:.gz::gzip
 #SUFFIX:.bz2:application/x-bzip2
+#SUFFIX:.zst:application/zstd
 #SUFFIX:.zip:application/zip
 #SUFFIX:.lzh:application/x-lzh
 #SUFFIX:.lha:application/x-lha
@@ -2407,6 +2408,7 @@ MINIMAL_COMMENTS:TRUE
 #	COMPRESS	For compress
 #	BZIP2		For bzip2
 #	BROTLI		For brotli
+#	ZSTD		For zstd
 #	ALL		All of the above.
 #PREFERRED_ENCODING:all
 
@@ -3574,6 +3576,10 @@ COLOR:6:brightred:black
 # This is the path used for DIRED mode to create a zip-archive from one or more
 # files, e.g., the program "unzip".
 
+.h2 ZSTD_PATH
+# This is the path used for DIRED mode and web connections to compress a file
+# to ".zst", e.g., the Unix command "zstd".
+
 .h1 Interaction
 
 .h2 FORCE_SSL_PROMPT
diff --git a/src/HTFWriter.c b/src/HTFWriter.c
index 0adaa80d5f..d8cd6017f2 100644
--- a/src/HTFWriter.c
+++ b/src/HTFWriter.c
@@ -49,6 +49,10 @@
 #include <brotli/decode.h>
 #endif
 
+#ifdef USE_ZSTD
+# include <zstd.h>
+#endif
+
 /* contains the name of the temp file which is being downloaded into */
 char *WWW_Download_File = NULL;
 BOOLEAN LYCancelDownload = FALSE;	/* exported to HTFormat.c in libWWW */
@@ -362,6 +366,123 @@ static void decompress_br(HTStream *me)
     }
 }
 
+static void decompress_zstd(HTStream *me) /* XXX too small a buffer; EINTR? */
+{
+    char *in_name = me->anchor->FileCache;
+    char copied[LY_MAXPATH];
+    FILE *ofp = LYOpenTemp(copied, ".tmp.zst", BIN_W);
+
+    if (ofp != NULL) {
+#ifdef USE_ZSTD
+	off_t dsize;
+	FILE *ifp;
+	ZSTD_DStream *izsp;
+	void *ibp, *obp;
+	size_t ibl, obl;
+
+	CTRACE((tfp, "decompressing '%s'\n", in_name));
+
+	ibl = ZSTD_DStreamInSize();
+	ibp = malloc(ibl);
+	if (ibp == NULL)
+		goto jout0;
+
+	obl = ZSTD_DStreamOutSize();
+	obp = malloc(obl);
+	if (obp == NULL)
+		goto jout1;
+
+	if ((izsp = ZSTD_createDStream()) == NULL)
+		goto jout2;
+	ZSTD_initDStream(izsp);
+
+	if ((ifp = fopen(in_name, BIN_R)) == NULL)
+		goto jout3;
+	CTRACE((tfp, "...opened '%s'\n", copied));
+
+	for (dsize = 0;;) {
+		ZSTD_inBuffer ib;
+		size_t r;
+
+		r = fread(ibp, 1, ibl, ifp);
+		if (r == 0)
+			break;
+		ib.src = ibp;
+		ib.size = r;
+		ib.pos = 0;
+
+		do {
+			ZSTD_outBuffer ob;
+			size_t w;
+
+			ob.dst = obp;
+			ob.size = obl;
+			ob.pos = 0;
+			r = ZSTD_decompressStream(izsp, &ob, &ib);
+			if (ZSTD_isError(r))
+				goto jout4;
+
+			for (w = 0; w < ob.pos;) {
+				size_t x;
+
+				x = fwrite(&((char*)obp)[w], 1, ob.pos - w, ofp);
+				if (x == 0)
+					goto jout4;
+				w += x;
+			}
+			dsize += w;
+		} while (ib.pos < ib.size);
+	}
+
+	LYCloseTempFP(ofp);
+	CTRACE((tfp, "...decompress %" PRI_off_t " to %" PRI_off_t "\n",
+		CAST_off_t (me->anchor->actual_length), CAST_off_t (dsize)));
+	if (LYRenameFile(copied, in_name) == 0)
+		me->anchor->actual_length = dsize;
+	(void) LYRemoveTemp(copied);
+
+jout4:	fclose(ifp);
+jout3:	ZSTD_freeDStream(izsp);
+jout2:	free(obp);
+jout1:	free(ibp);
+jout0:;
+
+#else
+# define FMT "%s -d --rm %s"
+	const char *program;
+
+	if ((program = HTGetProgramPath(ppZSTD)) == NULL) {
+	    HTAlert(ERROR_UNCOMPRESSING_TEMP);
+	} else if (LYCopyFile(in_name, copied) == 0) {
+	    char expanded[LY_MAXPATH];
+	    char *command = NULL;
+
+	    HTAddParam(&command, FMT, 1, program);
+	    HTAddParam(&command, FMT, 2, copied);
+	    HTEndParam(&command, FMT, 2);
+	    if (LYSystem(command) == 0) {
+		struct stat stat_buf;
+
+		strcpy(expanded, copied);
+		*strrchr(expanded, '.') = '\0';
+		if (LYRenameFile(expanded, in_name) != 0) {
+		    CTRACE((tfp, "rename failed %s to %s\n", expanded, in_name));
+		} else if (stat(in_name, &stat_buf) != 0) {
+		    CTRACE((tfp, "stat failed for %s\n", in_name));
+		} else {
+		    me->anchor->actual_length = stat_buf.st_size;
+		}
+	    } else {
+		CTRACE((tfp, "command failed: %s\n", command));
+	    }
+	    free(command);
+	    (void) LYRemoveTemp(copied);
+	}
+# undef FMT
+#endif
+    }
+}
+
 /*	Free an HTML object
  *	-------------------
  *
@@ -404,6 +525,11 @@ static void HTFWriter_free(HTStream *me)
 		   && IsCompressionFormat(me->input_format, cftBrotli)
 		   && !strcmp(me->anchor->content_encoding, "br")) {
 	    decompress_br(me);
+	} else if (me->anchor->FileCache != NULL
+		   && me->anchor->no_content_encoding == FALSE
+		   && IsCompressionFormat(me->input_format, cftZstd)
+		   && !strcmp(me->anchor->content_encoding, "zstd")) {
+	    decompress_zstd(me);
 	}
 #ifdef VMS
 	else if (0 == strcmp(me->end_command, "SaveVMSBinaryFile")) {
@@ -469,6 +595,16 @@ static void HTFWriter_free(HTStream *me)
 			path[len - 3] = '\0';
 			(void) remove(path);
 		    }
+		} else if (len > 4 && !strcasecomp(&path[len - 3], "zst")) {
+#ifdef USE_ZSTD
+		    if (!skip_loadfile) {
+			use_zread = YES;
+		    } else
+#endif /* USE_ZSTD */
+		    {
+			path[len - 4] = '\0';
+			(void) remove(path);
+		    }
 		} else if (len > 2 && !strcasecomp(&path[len - 1], "Z")) {
 		    path[len - 2] = '\0';
 		    (void) remove(path);
@@ -1323,6 +1459,13 @@ HTStream *HTCompressed(HTPresentation *pres,
 		    compress_suffix = "br";
 		}
 		break;
+	    case cftZstd:
+		if ((program = HTGetProgramPath(ppZSTD)) != NULL) {
+		    StrAllocCopy(uncompress_mask, program);
+		    StrAllocCat(uncompress_mask, " --rm -d %s");
+		    compress_suffix = "zst";
+		}
+		break;
 	    case cftCompress:
 		if ((program = HTGetProgramPath(ppUNCOMPRESS)) != NULL) {
 		    /*
@@ -1500,6 +1643,20 @@ HTStream *HTCompressed(HTPresentation *pres,
 	StrAllocCopy(me->end_command, "");
     } else
 #endif /* USE_ZLIB */
+#ifdef USE_ZSTD
+	if (compress_suffix[0] == 'z'	/* must be gzip */
+		&& compress_suffix[1] == 's'
+		&& compress_suffix[2] == 't'
+		&& compress_suffix[3] == '\0'
+	    && !me->viewer_command) {
+	/*
+	 * We won't call gzip or compress externally, so we don't need to
+	 * supply a command for it.
+	 */
+	StrAllocCopy(me->end_command, "");
+    } else
+#endif /* USE_ZLIB */
+
     {
 	me->end_command = NULL;
 	HTAddParam(&(me->end_command), uncompress_mask, 1, fnam);
diff --git a/src/HTInit.c b/src/HTInit.c
index 460bd1f9db..363e941913 100644
--- a/src/HTInit.c
+++ b/src/HTInit.c
@@ -1183,6 +1183,8 @@ void HTFileInit(void)
 
 	SET_SUFFIX1(".xz",	"application/x-xz",		"binary");
 
+	SET_SUFFIX1(".zst",	"application/zstd",		"binary");
+
 	SET_SUFFIX1(".lz",	"application/x-lzip",		"binary");
 	SET_SUFFIX1(".lzma",	"application/x-lzma",		"binary");
 
diff --git a/src/LYReadCFG.c b/src/LYReadCFG.c
index 24efbf51c6..acb69b71d6 100644
--- a/src/LYReadCFG.c
+++ b/src/LYReadCFG.c
@@ -1790,6 +1790,7 @@ static Config_Type Config_Table [] =
      PARSE_SET(RC_XHTML_PARSING,        LYxhtml_parsing),
      PARSE_PRG(RC_ZCAT_PATH,            ppZCAT),
      PARSE_PRG(RC_ZIP_PATH,             ppZIP),
+     PARSE_PRG(RC_ZSTD_PATH,            ppZSTD),
 
      PARSE_NIL
 };
diff --git a/src/LYrcFile.c b/src/LYrcFile.c
index b8cef3776a..dee3e86083 100644
--- a/src/LYrcFile.c
+++ b/src/LYrcFile.c
@@ -122,6 +122,9 @@ Config_Enum tbl_preferred_encoding[] = {
 #endif
 #if defined(USE_BROTLI) || defined(BROTLI_PATH)
     { "br",		encodingBROTLI },
+#endif
+#if defined(USE_ZSTD) || defined(ZSTD_PATH)
+    { "zstd",		encodingZSTD },
 #endif
     { "all",		encodingALL },
     { NULL,		-1 }
diff --git a/src/LYrcFile.h b/src/LYrcFile.h
index 1285a21666..fa7ac580e1 100644
--- a/src/LYrcFile.h
+++ b/src/LYrcFile.h
@@ -294,6 +294,7 @@
 #define RC_XLOADIMAGE_COMMAND           "xloadimage_command"
 #define RC_ZCAT_PATH                    "zcat_path"
 #define RC_ZIP_PATH                     "zip_path"
+#define RC_ZSTD_PATH                    "zstd_path"
 
 extern Config_Enum tbl_cookie_version[];
 extern Config_Enum tbl_force_prompt[];
-- 
2.45.2

Reply via email to