diff -ruN a/INSTALL b/INSTALL --- a/INSTALL 2021-09-23 13:03:23.000000000 -0500 +++ b/INSTALL 2026-05-29 18:09:12.237717849 -0500 @@ -103,7 +103,9 @@ 1. zlib for decompressing gzipped manual pages. -2. The fts(3) directory traversion functions. +2. libzstd for decompressing zstd-compressed manual pages. + +3. The fts(3) directory traversion functions. If your system does not have them, the bundled compatibility version will be used, so you need not worry in that case. But be careful: old glibc versions of fts(3) were known to be broken on 32bit platforms, @@ -111,7 +113,7 @@ That was presumably fixed in glibc-2.23. If you run into that problem, set "HAVE_FTS=0" in configure.local. -3. Marc Espie's ohash(3) library. +4. Marc Espie's ohash(3) library. If your system does not have it, the bundled compatibility version will be used, so you probably need not worry about it. diff -ruN a/Makefile b/Makefile --- a/Makefile 2021-09-23 13:03:23.000000000 -0500 +++ b/Makefile 2026-05-29 18:06:59.682203510 -0500 @@ -54,7 +54,8 @@ test-strsep.c \ test-strtonum.c \ test-vasprintf.c \ - test-wchar.c + test-wchar.c \ + test-zstd.c SRCS = arch.c \ att.c \ diff -ruN a/configure b/configure --- a/configure 2021-09-23 13:03:23.000000000 -0500 +++ b/configure 2026-05-29 18:06:59.682203510 -0500 @@ -50,6 +50,7 @@ LD_NANOSLEEP= LD_OHASH= LD_RECVMSG= +LD_ZSTD=-lzstd STATIC= BUILD_CGI=0 @@ -94,6 +95,7 @@ HAVE_SYS_ENDIAN= HAVE_VASPRINTF= HAVE_WCHAR= +HAVE_ZSTD= NEED_GNU_SOURCE=0 NEED_OPENBSD_SOURCE=0 @@ -331,6 +333,7 @@ runtest strsep STRSEP || true runtest strtonum STRTONUM "" -D_OPENBSD_SOURCE || true runtest vasprintf VASPRINTF "" -D_GNU_SOURCE || true +runtest zstd ZSTD "${LD_ZSTD}" || true # --- fts --- if [ "${1}" = "-depend" ]; then @@ -415,6 +418,11 @@ echo "FATAL: nanosleep: no" 1>&3 FATAL=1 fi +if [ "${HAVE_ZSTD}" -eq 0 ]; then + echo "FATAL: zstd: no" 1>&2 + echo "FATAL: zstd: no" 1>&3 + FATAL=1 +fi if [ ${BUILD_CATMAN} -gt 0 -a "${HAVE_RECVMSG}" -eq 0 ]; then echo "FATAL: recvmsg: no" 1>&2 echo "FATAL: recvmsg: no" 1>&3 @@ -430,7 +438,7 @@ [ "${FATAL}" -eq 0 ] || exit 1 # --- LDADD --- -LDADD="${LDADD} ${LD_NANOSLEEP} ${LD_RECVMSG} ${LD_OHASH} -lz" +LDADD="${LDADD} ${LD_NANOSLEEP} ${LD_RECVMSG} ${LD_OHASH} -lz ${LD_ZSTD}" echo "selected LDADD=\"${LDADD}\"" 1>&2 echo "selected LDADD=\"${LDADD}\"" 1>&3 echo 1>&3 @@ -506,6 +514,7 @@ #define HAVE_SYS_ENDIAN ${HAVE_SYS_ENDIAN} #define HAVE_VASPRINTF ${HAVE_VASPRINTF} #define HAVE_WCHAR ${HAVE_WCHAR} +#define HAVE_ZSTD ${HAVE_ZSTD} #define HAVE_OHASH ${HAVE_OHASH} #define NEED_XPG4_2 ${NEED_XPG4_2} diff -ruN a/main.c b/main.c --- a/main.c 2021-09-23 13:03:23.000000000 -0500 +++ b/main.c 2026-05-29 18:08:19.108307544 -0500 @@ -538,7 +538,10 @@ prio += 10; /* Wrong dir name. */ if (search.sec != NULL) { ep = strchr(sec, '\0'); - if (ep - sec > 3 && + if (ep - sec > 4 && + strncmp(ep - 4, ".zst", 4) == 0) + ep -= 4; + else if (ep - sec > 3 && strncmp(ep - 3, ".gz", 3) == 0) ep -= 3; if ((size_t)(ep - sec) < ssz + 3 || diff -ruN a/mandoc.3 b/mandoc.3 --- a/mandoc.3 2021-09-23 13:03:23.000000000 -0500 +++ b/mandoc.3 2026-05-29 18:09:19.497910732 -0500 @@ -273,10 +273,14 @@ If that fails and .Fa fname does not already end in -.Ql .gz , +.Ql .gz +or +.Ql .zst , try again after appending -.Ql .gz . -Save the information whether the file is zipped or not. +.Ql .gz +and +.Ql .zst . +Save the information whether the file is compressed or not. Return a file descriptor open for reading or -1 on failure. It can be passed to .Fn mparse_readfd diff -ruN a/mandocdb.c b/mandocdb.c --- a/mandocdb.c 2021-09-23 13:03:23.000000000 -0500 +++ b/mandocdb.c 2026-05-29 18:09:00.199398109 -0500 @@ -102,7 +102,7 @@ char *fsec; /* section from file name suffix */ struct mlink *next; /* singly linked list */ struct mpage *mpage; /* parent */ - int gzip; /* filename has a .gz suffix */ + int gzip; /* filename has a compression suffix */ enum form dform; /* format from directory */ enum form fform; /* format from file name suffix */ }; @@ -580,7 +580,7 @@ FTS *f; FTSENT *ff; struct mlink *mlink; - int gzip; + int compressed; enum form dform; char *dsec, *arch, *fsec, *cp; const char *path; @@ -645,13 +645,15 @@ say(path, "Extraneous file"); continue; } - gzip = 0; + compressed = 0; fsec = NULL; while (fsec == NULL) { fsec = strrchr(ff->fts_name, '.'); - if (fsec == NULL || strcmp(fsec+1, "gz")) + if (fsec == NULL || + (strcmp(fsec + 1, "gz") && + strcmp(fsec + 1, "zst"))) break; - gzip = 1; + compressed = 1; *fsec = '\0'; fsec = NULL; } @@ -697,7 +699,7 @@ mlink->arch = arch; mlink->name = ff->fts_name; mlink->fsec = fsec; - mlink->gzip = gzip; + mlink->gzip = compressed; mlink_add(mlink, ff->fts_statp); continue; @@ -962,9 +964,19 @@ * Now check the file suffix. * Suffix of `.0' indicates a catpage, `.1-9' is a manpage. */ - p = strrchr(start, '\0'); - while (p-- > start && *p != '/' && *p != '.') - continue; + if ((p = strrchr(start, '/')) == NULL) + p = start; + else + p++; + if ((p = strrchr(p, '.')) != NULL && + (strcmp(p + 1, "gz") == 0 || strcmp(p + 1, "zst") == 0)) + *p = '\0'; + + for (p = strchr(start, '\0'); p > start; ) { + p--; + if (*p == '/' || *p == '.') + break; + } if (*p == '.') { *p++ = '\0'; @@ -1213,6 +1225,12 @@ mlink_dest = ohash_find(&mlinks, ohash_qlookup(&mlinks, cp)); free(cp); + } + if (mlink_dest == NULL) { + mandoc_asprintf(&cp, "%s.zst", meta->sodest); + mlink_dest = ohash_find(&mlinks, + ohash_qlookup(&mlinks, cp)); + free(cp); } if (mlink_dest != NULL) { diff -ruN a/read.c b/read.c --- a/read.c 2021-09-23 13:03:23.000000000 -0500 +++ b/read.c 2026-05-29 18:09:41.222488117 -0500 @@ -38,6 +38,7 @@ #include #include #include +#include #include "mandoc_aux.h" #include "mandoc.h" @@ -60,6 +61,7 @@ const char *os_s; /* default operating system */ int options; /* parser options */ int gzip; /* current input file is gzipped */ + int zstd; /* current input file is zstd-compressed */ int filenc; /* encoding of the current file */ int reparse_count; /* finite interp. stack */ int line; /* line number in the file */ @@ -70,6 +72,7 @@ static void resize_buf(struct buf *, size_t); static int mparse_buf_r(struct mparse *, struct buf, size_t, int); static int read_whole_file(struct mparse *, int, struct buf *, int *); +static int read_zstd_file(struct mparse *, int, struct buf *); static void mparse_end(struct mparse *); @@ -432,6 +435,89 @@ } static int +read_zstd_file(struct mparse *curp, int fd, struct buf *fb) +{ + ZSTD_DStream *zds; + ZSTD_inBuffer zin; + ZSTD_outBuffer zout; + char *ibuf; + size_t ibufsz, off, ret; + ssize_t ssz; + int eof, retval; + + if ((zds = ZSTD_createDStream()) == NULL) { + mandoc_msg(MANDOCERR_READ, 0, 0, + "zstd: unable to allocate decompressor"); + return -1; + } + + ibufsz = ZSTD_DStreamInSize(); + ibuf = mandoc_malloc(ibufsz); + zin.src = ibuf; + zin.size = 0; + zin.pos = 0; + + fb->sz = 0; + fb->buf = NULL; + off = 0; + ret = 1; + eof = 0; + retval = -1; + + while (eof == 0 || zin.pos < zin.size) { + if (zin.pos == zin.size && eof == 0) { + if ((ssz = read(fd, ibuf, ibufsz)) == -1) { + mandoc_msg(MANDOCERR_READ, 0, 0, + "%s", strerror(errno)); + goto out; + } + if (ssz == 0) { + eof = 1; + continue; + } + zin.size = (size_t)ssz; + zin.pos = 0; + } + + if (off == fb->sz) { + if (fb->sz == (1U << 31)) { + mandoc_msg(MANDOCERR_TOOLARGE, 0, 0, NULL); + goto out; + } + resize_buf(fb, 65536); + } + + zout.dst = fb->buf + off; + zout.size = fb->sz - off; + zout.pos = 0; + ret = ZSTD_decompressStream(zds, &zout, &zin); + if (ZSTD_isError(ret)) { + mandoc_msg(MANDOCERR_READ, 0, 0, + "zstd: %s", ZSTD_getErrorName(ret)); + goto out; + } + off += zout.pos; + } + + if (ret != 0) + mandoc_msg(MANDOCERR_READ, 0, 0, + "zstd: unexpected end of file"); + else { + fb->sz = off; + retval = 0; + } + +out: + free(ibuf); + ZSTD_freeDStream(zds); + if (retval == -1) { + free(fb->buf); + fb->buf = NULL; + } + return retval; +} + +static int read_whole_file(struct mparse *curp, int fd, struct buf *fb, int *with_mmap) { struct stat st; @@ -452,7 +538,7 @@ * concerned that this is going to tank any machines. */ - if (curp->gzip == 0 && S_ISREG(st.st_mode)) { + if (curp->gzip == 0 && curp->zstd == 0 && S_ISREG(st.st_mode)) { if (st.st_size > 0x7fffffff) { mandoc_msg(MANDOCERR_TOOLARGE, 0, 0, NULL); return -1; @@ -464,6 +550,11 @@ return 0; } + if (curp->zstd) { + *with_mmap = 0; + return read_zstd_file(curp, fd, fb); + } + if (curp->gzip) { /* * Duplicating the file descriptor is required @@ -557,7 +648,7 @@ struct buf blk; struct buf *save_primary; - const char *save_filename, *cp; + const char *save_filename, *cp, *ep; size_t offset; int save_filenc, save_lineno; int with_mmap; @@ -565,12 +656,26 @@ if (recursion_depth > 64) { mandoc_msg(MANDOCERR_ROFFLOOP, curp->line, 0, NULL); return; - } else if (recursion_depth == 0 && - (cp = strrchr(filename, '.')) != NULL && - cp[1] >= '1' && cp[1] <= '9') - curp->man->filesec = cp[1]; - else - curp->man->filesec = '\0'; + } else if (recursion_depth == 0) { + ep = strrchr(filename, '.'); + if (ep != NULL && + (strcmp(ep + 1, "gz") == 0 || + strcmp(ep + 1, "zst") == 0)) { + for (cp = ep; cp > filename; cp--) + if (cp[-1] == '.') { + cp--; + break; + } + if (cp == filename) + cp = NULL; + } else + cp = ep; + if (cp != NULL && cp[1] >= '1' && cp[1] <= '9') + curp->man->filesec = cp[1]; + else + curp->man->filesec = '\0'; + } else + curp->man->filesec = '\0'; if (read_whole_file(curp, fd, &blk, &with_mmap) == -1) return; @@ -623,11 +728,13 @@ int mparse_open(struct mparse *curp, const char *file) { + const char *ep; char *cp; int fd, save_errno; - cp = strrchr(file, '.'); - curp->gzip = (cp != NULL && ! strcmp(cp + 1, "gz")); + ep = strrchr(file, '.'); + curp->gzip = (ep != NULL && ! strcmp(ep + 1, "gz")); + curp->zstd = (ep != NULL && ! strcmp(ep + 1, "zst")); /* First try to use the filename as it is. */ @@ -635,20 +742,27 @@ return fd; /* - * If that doesn't work and the filename doesn't - * already end in .gz, try appending .gz. + * If that doesn't work and the filename doesn't already + * end in .gz or .zst, try appending these suffixes. */ - if ( ! curp->gzip) { + if ( ! curp->gzip && ! curp->zstd) { save_errno = errno; mandoc_asprintf(&cp, "%s.gz", file); fd = open(cp, O_RDONLY); free(cp); - errno = save_errno; if (fd != -1) { curp->gzip = 1; return fd; } + mandoc_asprintf(&cp, "%s.zst", file); + fd = open(cp, O_RDONLY); + free(cp); + errno = save_errno; + if (fd != -1) { + curp->zstd = 1; + return fd; + } } /* Neither worked, give up. */ @@ -693,6 +807,7 @@ free_buf_list(curp->secondary); curp->secondary = NULL; curp->gzip = 0; + curp->zstd = 0; tag_alloc(); } diff -ruN a/test-zstd.c b/test-zstd.c --- a/test-zstd.c 1969-12-31 18:00:00.000000000 -0600 +++ b/test-zstd.c 2026-05-29 18:06:59.682203510 -0500 @@ -0,0 +1,7 @@ +#include + +int +main(void) +{ + return ZSTD_versionNumber() == 0; +}