xref: /lighttpd1.4/src/mod_dirlisting.c (revision 463c60ce)
1 /* fstatat() fdopendir() */
2 #if !defined(_XOPEN_SOURCE) || _XOPEN_SOURCE-0 < 700
3 #undef  _XOPEN_SOURCE
4 #define _XOPEN_SOURCE 700
5 /* NetBSD dirent.h improperly hides fdopendir() (POSIX.1-2008) declaration
6  * which should be visible with _XOPEN_SOURCE 700 or _POSIX_C_SOURCE 200809L */
7 #ifdef __NetBSD__
8 #define _NETBSD_SOURCE
9 #endif
10 #endif
11 
12 #include "first.h"
13 
14 #include "sys-time.h"
15 
16 #include "base.h"
17 #include "log.h"
18 #include "buffer.h"
19 #include "fdevent.h"
20 #include "http_chunk.h"
21 #include "http_etag.h"
22 #include "http_header.h"
23 #include "keyvalue.h"
24 #include "response.h"
25 
26 #include "plugin.h"
27 
28 #include "stat_cache.h"
29 
30 #include <errno.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <dirent.h>
34 #include <fcntl.h>
35 #include <unistd.h>
36 
37 #ifdef AT_FDCWD
38 #ifndef _ATFILE_SOURCE
39 #define _ATFILE_SOURCE
40 #endif
41 #endif
42 
43 #ifndef _D_EXACT_NAMLEN
44 #ifdef _DIRENT_HAVE_D_NAMLEN
45 #define _D_EXACT_NAMLEN(d) ((d)->d_namlen)
46 #else
47 #define _D_EXACT_NAMLEN(d) (strlen ((d)->d_name))
48 #endif
49 #endif
50 
51 /**
52  * this is a dirlisting for a lighttpd plugin
53  *
54  * Notes:
55  * - mod_dirlisting is a dir list implementation.  One size does not fit all.
56  *   mod_dirlisting aims to be somewhat flexible, but if special behavior
57  *   is needed, use custom CGI/FastCGI/SCGI to handle dir listing instead.
58  *   - backend daemon could implement custom caching
59  *   - backend daemon could monitor directory for changes (e.g. inotify)
60  *   - backend daemon or scripts could trigger when directory is modified
61  *     and could regenerate index.html (and mod_indexfile could be used
62  *     instead of mod_dirlisting)
63  * - basic alphabetical sorting (in C locale) is done on server side
64  *   in case client does not execute javascript
65  *   (otherwise, response could be streamed, which is not done)
66  *      (possible future dir-listing.* config option)
67  * - reading entire directory into memory for sorting large directory
68  *   can lead to large memory usage if many simultaneous requests occur
69  */
70 
71 struct dirlist_cache {
72 	int32_t max_age;
73 	buffer *path;
74 };
75 
76 typedef struct {
77 	char dir_listing;
78 	char json;
79 	char hide_dot_files;
80 	char hide_readme_file;
81 	char encode_readme;
82 	char hide_header_file;
83 	char encode_header;
84 	char auto_layout;
85 
86 	pcre_keyvalue_buffer *excludes;
87 
88 	const buffer *show_readme;
89 	const buffer *show_header;
90 	const buffer *external_css;
91 	const buffer *external_js;
92 	const buffer *encoding;
93 	const buffer *set_footer;
94 	const struct dirlist_cache *cache;
95 } plugin_config;
96 
97 typedef struct {
98 	PLUGIN_DATA;
99 	plugin_config defaults;
100 	plugin_config conf;
101 	int processing;
102 } plugin_data;
103 
104 typedef struct {
105 	uint32_t namelen;
106 	unix_time64_t mtime;
107 	off_t    size;
108 } dirls_entry_t;
109 
110 typedef struct {
111 	dirls_entry_t **ent;
112 	uint32_t used;
113 	uint32_t size;
114 } dirls_list_t;
115 
116 #define DIRLIST_ENT_NAME(ent)  ((char*)(ent) + sizeof(dirls_entry_t))
117 #define DIRLIST_BLOB_SIZE      16
118 
119 typedef struct {
120 	DIR *dp;
121 	dirls_list_t dirs;
122 	dirls_list_t files;
123 	char *path;
124 	char *path_file;
125 	int dfd; /*(dirfd() owned by (DIR *))*/
126 	uint32_t name_max;
127 	buffer *jb;
128 	int jcomma;
129 	int jfd;
130 	char *jfn;
131 	uint32_t jfn_len;
132 	plugin_config conf;
133 } handler_ctx;
134 
135 #define DIRLIST_BATCH 32
136 static int dirlist_max_in_progress;
137 
138 
139 static handler_ctx * mod_dirlisting_handler_ctx_init (plugin_data * const p) {
140     handler_ctx *hctx = calloc(1, sizeof(*hctx));
141     force_assert(hctx);
142     memcpy(&hctx->conf, &p->conf, sizeof(plugin_config));
143     return hctx;
144 }
145 
146 static void mod_dirlisting_handler_ctx_free (handler_ctx *hctx) {
147     if (hctx->dp)
148         closedir(hctx->dp);
149     if (hctx->files.ent) {
150         dirls_entry_t ** const ent = hctx->files.ent;
151         for (uint32_t i = 0, used = hctx->files.used; i < used; ++i)
152             free(ent[i]);
153         free(ent);
154     }
155     if (hctx->dirs.ent) {
156         dirls_entry_t ** const ent = hctx->dirs.ent;
157         for (uint32_t i = 0, used = hctx->dirs.used; i < used; ++i)
158             free(ent[i]);
159         free(ent);
160     }
161     if (hctx->jb) {
162         chunk_buffer_release(hctx->jb);
163         if (hctx->jfn) {
164             unlink(hctx->jfn);
165             free(hctx->jfn);
166         }
167         if (-1 != hctx->jfd)
168             close(hctx->jfd);
169     }
170     free(hctx->path);
171     free(hctx);
172 }
173 
174 
175 static struct dirlist_cache * mod_dirlisting_parse_cache(server *srv, const array *a) {
176     const data_unset *du;
177 
178     du = array_get_element_klen(a, CONST_STR_LEN("max-age"));
179     const int32_t max_age = config_plugin_value_to_int32(du, 15);
180 
181     buffer *path = NULL;
182     du = array_get_element_klen(a, CONST_STR_LEN("path"));
183     if (NULL == du) {
184         if (0 != max_age) {
185             log_error(srv->errh, __FILE__, __LINE__,
186               "dir-listing.cache must include \"path\"");
187             return NULL;
188         }
189     }
190     else {
191         if (du->type != TYPE_STRING) {
192             log_error(srv->errh, __FILE__, __LINE__,
193               "dir-listing.cache \"path\" must have string value");
194             return NULL;
195         }
196         path = &((data_string *)du)->value;
197         if (!stat_cache_path_isdir(path)) {
198             if (errno == ENOTDIR) {
199                 log_error(srv->errh, __FILE__, __LINE__,
200                   "dir-listing.cache \"path\" => \"%s\" is not a dir",
201                   path->ptr);
202                 return NULL;
203             }
204             if (errno == ENOENT) {
205                 log_error(srv->errh, __FILE__, __LINE__,
206                   "dir-listing.cache \"path\" => \"%s\" does not exist",
207                   path->ptr);
208                 /*(warning; not returning NULL)*/
209             }
210         }
211     }
212 
213     struct dirlist_cache * const cache = calloc(1, sizeof(struct dirlist_cache));
214     force_assert(cache);
215     cache->max_age = max_age;
216     cache->path = path;
217     return cache;
218 }
219 
220 
221 static pcre_keyvalue_buffer * mod_dirlisting_parse_excludes(server *srv, const array *a) {
222     const int pcre_jit = config_feature_bool(srv, "server.pcre_jit", 1);
223     pcre_keyvalue_buffer * const kvb = pcre_keyvalue_buffer_init();
224     buffer empty = { NULL, 0, 0 };
225     for (uint32_t j = 0; j < a->used; ++j) {
226         const data_string *ds = (data_string *)a->data[j];
227         if (!pcre_keyvalue_buffer_append(srv->errh, kvb, &ds->value, &empty,
228                                          pcre_jit)) {
229             log_error(srv->errh, __FILE__, __LINE__,
230               "pcre_compile failed for %s", ds->key.ptr);
231             pcre_keyvalue_buffer_free(kvb);
232             return NULL;
233         }
234     }
235     return kvb;
236 }
237 
238 #ifdef __COVERITY__
239 #include "burl.h"
240 #endif
241 
242 static int mod_dirlisting_exclude(pcre_keyvalue_buffer * const kvb, const char * const name, const uint32_t len) {
243     /*(re-use keyvalue.[ch] for match-only;
244      *  must have been configured with empty kvb 'value' during init)*/
245     buffer input = { NULL, len+1, 0 };
246     *(const char **)&input.ptr = name;
247     pcre_keyvalue_ctx ctx = { NULL, NULL, -1, 0, NULL, NULL };
248   #ifdef __COVERITY__
249     /*(again, must have been configured w/ empty kvb 'value' during init)*/
250     struct cond_match_t cache;
251     memset(&cache, 0, sizeof(cache));
252     struct burl_parts_t bp;
253     memset(&bp, 0, sizeof(bp));
254     ctx.cache = &cache;
255     ctx.burl = &bp;
256   #endif
257     /*(fail closed (simulate match to exclude) if there is an error)*/
258     return HANDLER_ERROR == pcre_keyvalue_buffer_process(kvb,&ctx,&input,NULL)
259         || -1 != ctx.m;
260 }
261 
262 
263 INIT_FUNC(mod_dirlisting_init) {
264     return calloc(1, sizeof(plugin_data));
265 }
266 
267 FREE_FUNC(mod_dirlisting_free) {
268     plugin_data * const p = p_d;
269     if (NULL == p->cvlist) return;
270     /* (init i to 0 if global context; to 1 to skip empty global context) */
271     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
272         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
273         for (; -1 != cpv->k_id; ++cpv) {
274             switch (cpv->k_id) {
275               case 2: /* dir-listing.exclude */
276                 if (cpv->vtype != T_CONFIG_LOCAL) continue;
277                 pcre_keyvalue_buffer_free(cpv->v.v);
278                 break;
279               case 15: /* dir-listing.cache */
280                 if (cpv->vtype != T_CONFIG_LOCAL) continue;
281                 free(cpv->v.v);
282                 break;
283               default:
284                 break;
285             }
286         }
287     }
288 }
289 
290 static void mod_dirlisting_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
291     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
292       case 0: /* dir-listing.activate */
293       case 1: /* server.dir-listing *//*(historical)*/
294         pconf->dir_listing = (char)cpv->v.u;
295         break;
296       case 2: /* dir-listing.exclude */
297         if (cpv->vtype == T_CONFIG_LOCAL)
298             pconf->excludes = cpv->v.v;
299         break;
300       case 3: /* dir-listing.hide-dotfiles */
301         pconf->hide_dot_files = (char)cpv->v.u;
302         break;
303       case 4: /* dir-listing.external-css */
304         pconf->external_css = cpv->v.b;
305         break;
306       case 5: /* dir-listing.external-js */
307         pconf->external_js = cpv->v.b;
308         break;
309       case 6: /* dir-listing.encoding */
310         pconf->encoding = cpv->v.b;
311         break;
312       case 7: /* dir-listing.show-readme */
313         pconf->show_readme = cpv->v.b;
314         break;
315       case 8: /* dir-listing.hide-readme-file */
316         pconf->hide_readme_file = (char)cpv->v.u;
317         break;
318       case 9: /* dir-listing.show-header */
319         pconf->show_header = cpv->v.b;
320         break;
321       case 10:/* dir-listing.hide-header-file */
322         pconf->hide_header_file = (char)cpv->v.u;
323         break;
324       case 11:/* dir-listing.set-footer */
325         pconf->set_footer = cpv->v.b;
326         break;
327       case 12:/* dir-listing.encode-readme */
328         pconf->encode_readme = (char)cpv->v.u;
329         break;
330       case 13:/* dir-listing.encode-header */
331         pconf->encode_header = (char)cpv->v.u;
332         break;
333       case 14:/* dir-listing.auto-layout */
334         pconf->auto_layout = (char)cpv->v.u;
335         break;
336       case 15:/* dir-listing.cache */
337         if (cpv->vtype == T_CONFIG_LOCAL)
338             pconf->cache = cpv->v.v;
339         break;
340       default:/* should not happen */
341         return;
342     }
343 }
344 
345 static void mod_dirlisting_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
346     do {
347         mod_dirlisting_merge_config_cpv(pconf, cpv);
348     } while ((++cpv)->k_id != -1);
349 }
350 
351 static void mod_dirlisting_patch_config(request_st * const r, plugin_data * const p) {
352     memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
353     for (int i = 1, used = p->nconfig; i < used; ++i) {
354         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
355             mod_dirlisting_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]);
356     }
357 }
358 
359 SETDEFAULTS_FUNC(mod_dirlisting_set_defaults) {
360     static const config_plugin_keys_t cpk[] = {
361       { CONST_STR_LEN("dir-listing.activate"),
362         T_CONFIG_BOOL,
363         T_CONFIG_SCOPE_CONNECTION }
364      ,{ CONST_STR_LEN("server.dir-listing"),
365         T_CONFIG_BOOL,
366         T_CONFIG_SCOPE_CONNECTION }
367      ,{ CONST_STR_LEN("dir-listing.exclude"),
368         T_CONFIG_ARRAY_VLIST,
369         T_CONFIG_SCOPE_CONNECTION }
370      ,{ CONST_STR_LEN("dir-listing.hide-dotfiles"),
371         T_CONFIG_BOOL,
372         T_CONFIG_SCOPE_CONNECTION }
373      ,{ CONST_STR_LEN("dir-listing.external-css"),
374         T_CONFIG_STRING,
375         T_CONFIG_SCOPE_CONNECTION }
376      ,{ CONST_STR_LEN("dir-listing.external-js"),
377         T_CONFIG_STRING,
378         T_CONFIG_SCOPE_CONNECTION }
379      ,{ CONST_STR_LEN("dir-listing.encoding"),
380         T_CONFIG_STRING,
381         T_CONFIG_SCOPE_CONNECTION }
382      ,{ CONST_STR_LEN("dir-listing.show-readme"),
383         T_CONFIG_STRING,
384         T_CONFIG_SCOPE_CONNECTION }
385      ,{ CONST_STR_LEN("dir-listing.hide-readme-file"),
386         T_CONFIG_BOOL,
387         T_CONFIG_SCOPE_CONNECTION }
388      ,{ CONST_STR_LEN("dir-listing.show-header"),
389         T_CONFIG_STRING,
390         T_CONFIG_SCOPE_CONNECTION }
391      ,{ CONST_STR_LEN("dir-listing.hide-header-file"),
392         T_CONFIG_BOOL,
393         T_CONFIG_SCOPE_CONNECTION }
394      ,{ CONST_STR_LEN("dir-listing.set-footer"),
395         T_CONFIG_STRING,
396         T_CONFIG_SCOPE_CONNECTION }
397      ,{ CONST_STR_LEN("dir-listing.encode-readme"),
398         T_CONFIG_BOOL,
399         T_CONFIG_SCOPE_CONNECTION }
400      ,{ CONST_STR_LEN("dir-listing.encode-header"),
401         T_CONFIG_BOOL,
402         T_CONFIG_SCOPE_CONNECTION }
403      ,{ CONST_STR_LEN("dir-listing.auto-layout"),
404         T_CONFIG_BOOL,
405         T_CONFIG_SCOPE_CONNECTION }
406      ,{ CONST_STR_LEN("dir-listing.cache"),
407         T_CONFIG_ARRAY_KVANY,
408         T_CONFIG_SCOPE_CONNECTION }
409      ,{ NULL, 0,
410         T_CONFIG_UNSET,
411         T_CONFIG_SCOPE_UNSET }
412     };
413 
414     plugin_data * const p = p_d;
415     if (!config_plugin_values_init(srv, p, cpk, "mod_dirlisting"))
416         return HANDLER_ERROR;
417 
418     /* process and validate config directives
419      * (init i to 0 if global context; to 1 to skip empty global context) */
420     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
421         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
422         for (; -1 != cpv->k_id; ++cpv) {
423             switch (cpv->k_id) {
424               case 0: /* dir-listing.activate */
425               case 1: /* server.dir-listing *//*(historical)*/
426                 break;
427               case 2: /* dir-listing.exclude */
428                 cpv->v.v = mod_dirlisting_parse_excludes(srv, cpv->v.a);
429                 if (NULL == cpv->v.v) return HANDLER_ERROR;
430                 cpv->vtype = T_CONFIG_LOCAL;
431                 break;
432               case 3: /* dir-listing.hide-dotfiles */
433                 break;
434               case 4: /* dir-listing.external-css */
435               case 5: /* dir-listing.external-js */
436               case 6: /* dir-listing.encoding */
437                 if (buffer_is_blank(cpv->v.b))
438                     cpv->v.b = NULL;
439                 break;
440               case 7: /* dir-listing.show-readme */
441                 if (!buffer_is_blank(cpv->v.b)) {
442                     buffer *b;
443                     *(const buffer **)&b = cpv->v.b;
444                     if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
445                         buffer_copy_string_len(b, CONST_STR_LEN("README.txt"));
446                     else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
447                         buffer_clear(b);
448                 }
449                 else
450                     cpv->v.b = NULL;
451                 break;
452               case 8: /* dir-listing.hide-readme-file */
453                 break;
454               case 9: /* dir-listing.show-header */
455                 if (!buffer_is_blank(cpv->v.b)) {
456                     buffer *b;
457                     *(const buffer **)&b = cpv->v.b;
458                     if (buffer_is_equal_string(b, CONST_STR_LEN("enable")))
459                         buffer_copy_string_len(b, CONST_STR_LEN("HEADER.txt"));
460                     else if (buffer_is_equal_string(b,CONST_STR_LEN("disable")))
461                         buffer_clear(b);
462                 }
463                 else
464                     cpv->v.b = NULL;
465                 break;
466               case 10:/* dir-listing.hide-header-file */
467                 break;
468               case 11:/* dir-listing.set-footer */
469                 if (buffer_is_blank(cpv->v.b))
470                     cpv->v.b = NULL;
471                 break;
472               case 12:/* dir-listing.encode-readme */
473               case 13:/* dir-listing.encode-header */
474               case 14:/* dir-listing.auto-layout */
475                 break;
476               case 15:/* dir-listing.cache */
477                 cpv->v.v = mod_dirlisting_parse_cache(srv, cpv->v.a);
478                 if (NULL == cpv->v.v) return HANDLER_ERROR;
479                 if (0 == ((struct dirlist_cache *)cpv->v.v)->max_age) {
480                     free(cpv->v.v);
481                     cpv->v.v = NULL; /*(to disable after having been enabled)*/
482                 }
483                 cpv->vtype = T_CONFIG_LOCAL;
484                 break;
485               default:/* should not happen */
486                 break;
487             }
488         }
489     }
490 
491     dirlist_max_in_progress = srv->srvconf.max_conns >> 4;
492     if (0 == dirlist_max_in_progress) dirlist_max_in_progress = 1;
493 
494     p->defaults.dir_listing = 0;
495     p->defaults.json = 0;
496     p->defaults.hide_dot_files = 1;
497     p->defaults.hide_readme_file = 0;
498     p->defaults.hide_header_file = 0;
499     p->defaults.encode_readme = 1;
500     p->defaults.encode_header = 1;
501     p->defaults.auto_layout = 1;
502 
503     /* initialize p->defaults from global config context */
504     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
505         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
506         if (-1 != cpv->k_id)
507             mod_dirlisting_merge_config(&p->defaults, cpv);
508     }
509 
510     return HANDLER_GO_ON;
511 }
512 
513 /* simple combsort algorithm */
514 static void http_dirls_sort(dirls_entry_t **ent, int num) {
515 	int gap = num;
516 	int i, j;
517 	int swapped;
518 	dirls_entry_t *tmp;
519 
520 	do {
521 		gap = (gap * 10) / 13;
522 		if (gap == 9 || gap == 10)
523 			gap = 11;
524 		if (gap < 1)
525 			gap = 1;
526 		swapped = 0;
527 
528 		for (i = 0; i < num - gap; i++) {
529 			j = i + gap;
530 			if (strcmp(DIRLIST_ENT_NAME(ent[i]), DIRLIST_ENT_NAME(ent[j])) > 0) {
531 				tmp = ent[i];
532 				ent[i] = ent[j];
533 				ent[j] = tmp;
534 				swapped = 1;
535 			}
536 		}
537 
538 	} while (gap > 1 || swapped);
539 }
540 
541 /* buffer must be able to hold "999.9K"
542  * conversion is simple but not perfect
543  */
544 static size_t http_list_directory_sizefmt(char *buf, size_t bufsz, off_t size) {
545 	int remain;
546 	int u = -1;  /* u will always increment at least once */
547 	size_t buflen;
548 
549 	if (0 < size && size < 100)
550 		size += 99;
551 
552 	do {
553 		remain = (int)(size & 1023);
554 		size >>= 10;
555 		++u;
556 	} while (size & ~1023);
557 
558 	remain /= 100;
559 	if (remain > 9)
560 		remain = 9;
561 	if (size > 999) {
562 		size   = 0;
563 		remain = 9;
564 		u++;
565 	}
566 
567 	buflen = li_itostrn(buf, bufsz, size);
568 	if (buflen + 3 >= bufsz) return buflen;
569 	buf[buflen+0] = '.';
570 	buf[buflen+1] = remain + '0';
571 	buf[buflen+2] = "KMGTPE"[u];  /* Kilo, Mega, Giga, Tera, Peta, Exa */
572 	buf[buflen+3] = '\0';
573 
574 	return buflen + 3;
575 }
576 
577 static void http_list_directory_include_file(request_st * const r, const handler_ctx * const p, int is_header) {
578     const buffer *path;
579     int encode = 0;
580     if (is_header) {
581         path = p->conf.show_header;
582         encode = p->conf.encode_header;
583     }
584     else {
585         path = p->conf.show_readme;
586         encode = p->conf.encode_readme;
587     }
588     if (NULL == path) return;
589 
590     uint32_t len = 0;
591     if (path->ptr[0] != '/') { /* temporarily extend r->physical.path */
592         len = buffer_clen(&r->physical.path);
593         buffer_append_path_len(&r->physical.path, BUF_PTR_LEN(path));
594         path = &r->physical.path;
595     }
596     stat_cache_entry * const sce =
597       stat_cache_get_entry_open(path, r->conf.follow_symlink);
598     if (len)
599         buffer_truncate(&r->physical.path, len);
600     if (NULL == sce || sce->fd < 0 || 0 == sce->st.st_size)
601         return;
602 
603     chunkqueue * const cq = &r->write_queue;
604     if (encode) {
605         if (is_header)
606             chunkqueue_append_mem(cq, CONST_STR_LEN("<pre class=\"header\">"));
607         else
608             chunkqueue_append_mem(cq, CONST_STR_LEN("<pre class=\"readme\">"));
609 
610         /* Note: encoding a very large file may cause lighttpd to pause handling
611          * other requests while lighttpd encodes the file, especially if file is
612          * on a remote filesystem */
613 
614         /* encoding can consume 6x file size in worst case scenario,
615          * so send encoded contents of files > 32k to tempfiles) */
616         buffer * const tb = r->tmp_buf;
617         buffer * const out = sce->st.st_size <= 32768
618           ? chunkqueue_append_buffer_open(cq)
619           : tb;
620         buffer_clear(out);
621         const int fd = sce->fd;
622         ssize_t rd;
623         char buf[8192];
624         while ((rd = read(fd, buf, sizeof(buf))) > 0) {
625             buffer_append_string_encoded(out, buf, (size_t)rd, ENCODING_MINIMAL_XML);
626             if (out == tb) {
627                 if (0 != chunkqueue_append_mem_to_tempfile(cq,
628                                                            BUF_PTR_LEN(out),
629                                                            r->conf.errh))
630                     break;
631                 buffer_clear(out);
632             }
633         }
634         if (out != tb)
635             chunkqueue_append_buffer_commit(cq);
636 
637         chunkqueue_append_mem(cq, CONST_STR_LEN("</pre>"));
638     }
639     else {
640         (void)http_chunk_append_file_ref(r, sce);
641     }
642 }
643 
644 /* portions copied from mod_status
645  * modified and specialized for stable dirlist sorting by name */
646 static const char js_simple_table_resort[] = \
647 "var click_column;\n" \
648 "var name_column = 0;\n" \
649 "var date_column = 1;\n" \
650 "var size_column = 2;\n" \
651 "var type_column = 3;\n" \
652 "var prev_span = null;\n" \
653 "\n" \
654 "if (typeof(String.prototype.localeCompare) === 'undefined') {\n" \
655 " String.prototype.localeCompare = function(str, locale, options) {\n" \
656 "   return ((this == str) ? 0 : ((this > str) ? 1 : -1));\n" \
657 " };\n" \
658 "}\n" \
659 "\n" \
660 "if (typeof(String.prototype.toLocaleUpperCase) === 'undefined') {\n" \
661 " String.prototype.toLocaleUpperCase = function() {\n" \
662 "  return this.toUpperCase();\n" \
663 " };\n" \
664 "}\n" \
665 "\n" \
666 "function get_inner_text(el) {\n" \
667 " if((typeof el == 'string')||(typeof el == 'undefined'))\n" \
668 "  return el;\n" \
669 " if(el.innerText)\n" \
670 "  return el.innerText;\n" \
671 " else {\n" \
672 "  var str = \"\";\n" \
673 "  var cs = el.childNodes;\n" \
674 "  var l = cs.length;\n" \
675 "  for (var i=0;i<l;i++) {\n" \
676 "   if (cs[i].nodeType==1) str += get_inner_text(cs[i]);\n" \
677 "   else if (cs[i].nodeType==3) str += cs[i].nodeValue;\n" \
678 "  }\n" \
679 " }\n" \
680 " return str;\n" \
681 "}\n" \
682 "\n" \
683 "function isdigit(c) {\n" \
684 " return (c >= '0' && c <= '9');\n" \
685 "}\n" \
686 "\n" \
687 "function unit_multiplier(unit) {\n" \
688 " return (unit=='K') ? 1000\n" \
689 "      : (unit=='M') ? 1000000\n" \
690 "      : (unit=='G') ? 1000000000\n" \
691 "      : (unit=='T') ? 1000000000000\n" \
692 "      : (unit=='P') ? 1000000000000000\n" \
693 "      : (unit=='E') ? 1000000000000000000 : 1;\n" \
694 "}\n" \
695 "\n" \
696 "var li_date_regex=/(\\d{4})-(\\w{3})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})/;\n" \
697 "\n" \
698 "var li_mon = ['Jan','Feb','Mar','Apr','May','Jun',\n" \
699 "              'Jul','Aug','Sep','Oct','Nov','Dec'];\n" \
700 "\n" \
701 "function li_mon_num(mon) {\n" \
702 " var i; for (i = 0; i < 12 && mon != li_mon[i]; ++i); return i;\n" \
703 "}\n" \
704 "\n" \
705 "function li_date_cmp(s1, s2) {\n" \
706 " var dp1 = li_date_regex.exec(s1)\n" \
707 " var dp2 = li_date_regex.exec(s2)\n" \
708 " for (var i = 1; i < 7; ++i) {\n" \
709 "  var cmp = (2 != i)\n" \
710 "   ? parseInt(dp1[i]) - parseInt(dp2[i])\n" \
711 "   : li_mon_num(dp1[2]) - li_mon_num(dp2[2]);\n" \
712 "  if (0 != cmp) return cmp;\n" \
713 " }\n" \
714 " return 0;\n" \
715 "}\n" \
716 "\n" \
717 "function sortfn_then_by_name(a,b,sort_column) {\n" \
718 " if (sort_column == name_column || sort_column == type_column) {\n" \
719 "  var ad = (a.cells[type_column].innerHTML === 'Directory');\n" \
720 "  var bd = (b.cells[type_column].innerHTML === 'Directory');\n" \
721 "  if (ad != bd) return (ad ? -1 : 1);\n" \
722 " }\n" \
723 " var at = get_inner_text(a.cells[sort_column]);\n" \
724 " var bt = get_inner_text(b.cells[sort_column]);\n" \
725 " var cmp;\n" \
726 " if (sort_column == name_column) {\n" \
727 "  if (at == '../') return -1;\n" \
728 "  if (bt == '../') return  1;\n" \
729 " }\n" \
730 " if (a.cells[sort_column].className == 'int') {\n" \
731 "  cmp = parseInt(at)-parseInt(bt);\n" \
732 " } else if (sort_column == date_column) {\n" \
733 "  var ad = isdigit(at.substr(0,1));\n" \
734 "  var bd = isdigit(bt.substr(0,1));\n" \
735 "  if (ad != bd) return (!ad ? -1 : 1);\n" \
736 "  cmp = li_date_cmp(at,bt);\n" \
737 " } else if (sort_column == size_column) {\n" \
738 "  var ai = parseInt(at, 10) * unit_multiplier(at.substr(-1,1));\n" \
739 "  var bi = parseInt(bt, 10) * unit_multiplier(bt.substr(-1,1));\n" \
740 "  if (at.substr(0,1) == '-') ai = -1;\n" \
741 "  if (bt.substr(0,1) == '-') bi = -1;\n" \
742 "  cmp = ai - bi;\n" \
743 " } else {\n" \
744 "  cmp = at.toLocaleUpperCase().localeCompare(bt.toLocaleUpperCase());\n" \
745 "  if (0 != cmp) return cmp;\n" \
746 "  cmp = at.localeCompare(bt);\n" \
747 " }\n" \
748 " if (0 != cmp || sort_column == name_column) return cmp;\n" \
749 " return sortfn_then_by_name(a,b,name_column);\n" \
750 "}\n" \
751 "\n" \
752 "function sortfn(a,b) {\n" \
753 " return sortfn_then_by_name(a,b,click_column);\n" \
754 "}\n" \
755 "\n" \
756 "function resort(lnk) {\n" \
757 " var span = lnk.childNodes[1];\n" \
758 " var table = lnk.parentNode.parentNode.parentNode.parentNode;\n" \
759 " var rows = new Array();\n" \
760 " for (var j=1;j<table.rows.length;j++)\n" \
761 "  rows[j-1] = table.rows[j];\n" \
762 " click_column = lnk.parentNode.cellIndex;\n" \
763 " rows.sort(sortfn);\n" \
764 "\n" \
765 " if (prev_span != null) prev_span.innerHTML = '';\n" \
766 " if (span.getAttribute('sortdir')=='down') {\n" \
767 "  span.innerHTML = '&uarr;';\n" \
768 "  span.setAttribute('sortdir','up');\n" \
769 "  rows.reverse();\n" \
770 " } else {\n" \
771 "  span.innerHTML = '&darr;';\n" \
772 "  span.setAttribute('sortdir','down');\n" \
773 " }\n" \
774 " for (var i=0;i<rows.length;i++)\n" \
775 "  table.tBodies[0].appendChild(rows[i]);\n" \
776 " prev_span = span;\n" \
777 "}\n";
778 
779 /* portions copied from mod_dirlist (lighttpd2) */
780 static const char js_simple_table_init_sort[] = \
781 "\n" \
782 "function init_sort(init_sort_column, ascending) {\n" \
783 " var tables = document.getElementsByTagName(\"table\");\n" \
784 " for (var i = 0; i < tables.length; i++) {\n" \
785 "  var table = tables[i];\n" \
786 "  //var c = table.getAttribute(\"class\")\n" \
787 "  //if (-1 != c.split(\" \").indexOf(\"sort\")) {\n" \
788 "   var row = table.rows[0].cells;\n" \
789 "   for (var j = 0; j < row.length; j++) {\n" \
790 "    var n = row[j];\n" \
791 "    if (n.childNodes.length == 1 && n.childNodes[0].nodeType == 3) {\n" \
792 "     var link = document.createElement(\"a\");\n" \
793 "     var title = n.childNodes[0].nodeValue.replace(/:$/, \"\");\n" \
794 "     link.appendChild(document.createTextNode(title));\n" \
795 "     link.setAttribute(\"href\", \"#\");\n" \
796 "     link.setAttribute(\"class\", \"sortheader\");\n" \
797 "     link.setAttribute(\"onclick\", \"resort(this);return false;\");\n" \
798 "     var arrow = document.createElement(\"span\");\n" \
799 "     arrow.setAttribute(\"class\", \"sortarrow\");\n" \
800 "     arrow.appendChild(document.createTextNode(\":\"));\n" \
801 "     link.appendChild(arrow)\n" \
802 "     n.replaceChild(link, n.firstChild);\n" \
803 "    }\n" \
804 "   }\n" \
805 "   var lnk = row[init_sort_column].firstChild;\n" \
806 "   if (ascending) {\n" \
807 "    var span = lnk.childNodes[1];\n" \
808 "    span.setAttribute('sortdir','down');\n" \
809 "   }\n" \
810 "   resort(lnk);\n" \
811 "  //}\n" \
812 " }\n" \
813 "}\n" \
814 "\n" \
815 "function init_sort_from_query() {\n" \
816 "  var urlParams = new URLSearchParams(location.search);\n" \
817 "  var c = 0;\n" \
818 "  var o = 0;\n" \
819 "  switch (urlParams.get('C')) {\n" \
820 "    case \"N\": c=0; break;\n" \
821 "    case \"M\": c=1; break;\n" \
822 "    case \"S\": c=2; break;\n" \
823 "    case \"T\":\n" \
824 "    case \"D\": c=3; break;\n" \
825 "  }\n" \
826 "  switch (urlParams.get('O')) {\n" \
827 "    case \"A\": o=1; break;\n" \
828 "    case \"D\": o=0; break;\n" \
829 "  }\n" \
830 "  init_sort(c,o);\n" \
831 "}\n" \
832 "init_sort_from_query();\n";
833 
834 
835 static void http_dirlist_append_js_table_resort (buffer * const b) {
836 	struct const_iovec iov[] = {
837 	  { CONST_STR_LEN("\n<script type=\"text/javascript\">\n// <!--\n\n") }
838 	 ,{ CONST_STR_LEN(js_simple_table_resort) }
839 	 ,{ CONST_STR_LEN(js_simple_table_init_sort) }
840 	 ,{ CONST_STR_LEN("\n// -->\n</script>\n\n") }
841 	};
842 	buffer_append_iovec(b, iov, sizeof(iov)/sizeof(*iov));
843 }
844 
845 static void http_list_directory_header(request_st * const r, const handler_ctx * const p) {
846 
847 	chunkqueue * const cq = &r->write_queue;
848 	if (p->conf.auto_layout) {
849 		buffer * const out = chunkqueue_append_buffer_open(cq);
850 		buffer_append_string_len(out, CONST_STR_LEN(
851 			"<!DOCTYPE html>\n"
852 			"<html>\n"
853 			"<head>\n"
854 		));
855 		if (p->conf.encoding) {
856 			buffer_append_str3(out,
857 			  CONST_STR_LEN("<meta charset=\""),
858 			  BUF_PTR_LEN(p->conf.encoding),
859 			  CONST_STR_LEN("\">\n"));
860 		}
861 		buffer_append_string_len(out, CONST_STR_LEN("<title>Index of "));
862 		buffer_append_string_encoded(out, BUF_PTR_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
863 		buffer_append_string_len(out, CONST_STR_LEN("</title>\n"));
864 
865 		if (p->conf.external_css) {
866 			buffer_append_str3(out,
867 			  CONST_STR_LEN("<meta name=\"viewport\" content=\"initial-scale=1\">"
868 			                "<link rel=\"stylesheet\" type=\"text/css\" href=\""),
869 			  BUF_PTR_LEN(p->conf.external_css),
870 			  CONST_STR_LEN("\">\n"));
871 		} else {
872 			buffer_append_string_len(out, CONST_STR_LEN(
873 				"<style type=\"text/css\">\n"
874 				"a, a:active {text-decoration: none; color: blue;}\n"
875 				"a:visited {color: #48468F;}\n"
876 				"a:hover, a:focus {text-decoration: underline; color: red;}\n"
877 				"body {background-color: #F5F5F5;}\n"
878 				"h2 {margin-bottom: 12px;}\n"
879 				"table {margin-left: 12px;}\n"
880 				"th, td {"
881 				" font: 90% monospace;"
882 				" text-align: left;"
883 				"}\n"
884 				"th {"
885 				" font-weight: bold;"
886 				" padding-right: 14px;"
887 				" padding-bottom: 3px;"
888 				"}\n"
889 				"td {padding-right: 14px;}\n"
890 				"td.s, th.s {text-align: right;}\n"
891 				"div.list {"
892 				" background-color: white;"
893 				" border-top: 1px solid #646464;"
894 				" border-bottom: 1px solid #646464;"
895 				" padding-top: 10px;"
896 				" padding-bottom: 14px;"
897 				"}\n"
898 				"div.foot {"
899 				" font: 90% monospace;"
900 				" color: #787878;"
901 				" padding-top: 4px;"
902 				"}\n"
903 				"</style>\n"
904 			));
905 		}
906 
907 		buffer_append_string_len(out, CONST_STR_LEN("</head>\n<body>\n"));
908 		chunkqueue_append_buffer_commit(cq);
909 	}
910 
911 	if (p->conf.show_header) {
912 		http_list_directory_include_file(r, p, 1);/*0 for readme; 1 for header*/
913 	}
914 
915 	buffer * const out = chunkqueue_append_buffer_open(cq);
916 	buffer_append_string_len(out, CONST_STR_LEN("<h2>Index of "));
917 	buffer_append_string_encoded(out, BUF_PTR_LEN(&r->uri.path), ENCODING_MINIMAL_XML);
918 	buffer_append_string_len(out, CONST_STR_LEN(
919 		"</h2>\n"
920 		"<div class=\"list\">\n"
921 		"<table summary=\"Directory Listing\" cellpadding=\"0\" cellspacing=\"0\">\n"
922 		"<thead>"
923 		"<tr>"
924 			"<th class=\"n\">Name</th>"
925 			"<th class=\"m\">Last Modified</th>"
926 			"<th class=\"s\">Size</th>"
927 			"<th class=\"t\">Type</th>"
928 		"</tr>"
929 		"</thead>\n"
930 		"<tbody>\n"
931 	));
932 	if (!buffer_is_equal_string(&r->uri.path, CONST_STR_LEN("/"))) {
933 		buffer_append_string_len(out, CONST_STR_LEN(
934 		"<tr class=\"d\">"
935 			"<td class=\"n\"><a href=\"../\">..</a>/</td>"
936 			"<td class=\"m\">&nbsp;</td>"
937 			"<td class=\"s\">- &nbsp;</td>"
938 			"<td class=\"t\">Directory</td>"
939 		"</tr>\n"
940 		));
941 	}
942 	chunkqueue_append_buffer_commit(cq);
943 }
944 
945 static void http_list_directory_footer(request_st * const r, const handler_ctx * const p) {
946 
947 	chunkqueue * const cq = &r->write_queue;
948 	chunkqueue_append_mem(cq, CONST_STR_LEN(
949 		"</tbody>\n"
950 		"</table>\n"
951 		"</div>\n"
952 	));
953 
954 	if (p->conf.show_readme) {
955 		http_list_directory_include_file(r, p, 0);/*0 for readme; 1 for header*/
956 	}
957 
958 	if (p->conf.auto_layout) {
959 		buffer * const out = chunkqueue_append_buffer_open(cq);
960 		const buffer * const footer =
961 		  p->conf.set_footer
962 		    ? p->conf.set_footer
963 		    : r->conf.server_tag
964 		        ? r->conf.server_tag
965 		        : NULL;
966 		if (footer)
967 			buffer_append_str3(out,
968 			  CONST_STR_LEN("<div class=\"foot\">"),
969 			  BUF_PTR_LEN(footer),
970 			  CONST_STR_LEN("</div>\n"));
971 
972 		if (p->conf.external_js)
973 			buffer_append_str3(out,
974 			  CONST_STR_LEN("<script type=\"text/javascript\" src=\""),
975 			  BUF_PTR_LEN(p->conf.external_js),
976 			  CONST_STR_LEN("\"></script>\n"));
977 		else
978 			http_dirlist_append_js_table_resort(out);
979 
980 		buffer_append_string_len(out, CONST_STR_LEN(
981 			"</body>\n"
982 			"</html>\n"
983 		));
984 		chunkqueue_append_buffer_commit(cq);
985 	}
986 }
987 
988 static int http_open_directory(request_st * const r, handler_ctx * const hctx) {
989     const uint32_t dlen = buffer_clen(&r->physical.path);
990 #if defined __WIN32
991     hctx->name_max = FILENAME_MAX;
992 #else
993 #ifndef PATH_MAX
994 #define PATH_MAX 4096
995 #endif
996     /* allocate based on PATH_MAX rather than pathconf() to get _PC_NAME_MAX */
997     hctx->name_max = PATH_MAX - dlen - 1;
998 #endif
999     hctx->path = malloc(dlen + hctx->name_max + 1);
1000     force_assert(NULL != hctx->path);
1001     memcpy(hctx->path, r->physical.path.ptr, dlen+1);
1002   #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) || !defined(_ATFILE_SOURCE)
1003     hctx->path_file = hctx->path + dlen;
1004   #endif
1005 
1006   #ifndef _ATFILE_SOURCE /*(not using fdopendir unless _ATFILE_SOURCE)*/
1007     hctx->dfd = -1;
1008     hctx->dp = opendir(hctx->path);
1009   #else
1010     hctx->dfd = fdevent_open_dirname(hctx->path, r->conf.follow_symlink);
1011     hctx->dp = (hctx->dfd >= 0) ? fdopendir(hctx->dfd) : NULL;
1012   #endif
1013     if (NULL == hctx->dp) {
1014         log_perror(r->conf.errh, __FILE__, __LINE__, "opendir %s", hctx->path);
1015         if (hctx->dfd >= 0) {
1016             close(hctx->dfd);
1017             hctx->dfd = -1;
1018         }
1019         return -1;
1020     }
1021 
1022     if (hctx->conf.json) return 0;
1023 
1024     dirls_list_t * const dirs = &hctx->dirs;
1025     dirls_list_t * const files = &hctx->files;
1026     dirs->ent   =
1027       (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
1028     force_assert(dirs->ent);
1029     dirs->size  = DIRLIST_BLOB_SIZE;
1030     dirs->used  = 0;
1031     files->ent  =
1032       (dirls_entry_t**) malloc(sizeof(dirls_entry_t*) * DIRLIST_BLOB_SIZE);
1033     force_assert(files->ent);
1034     files->size = DIRLIST_BLOB_SIZE;
1035     files->used = 0;
1036 
1037     return 0;
1038 }
1039 
1040 static int http_read_directory(handler_ctx * const p) {
1041 	struct dirent *dent;
1042 	const int hide_dotfiles = p->conf.hide_dot_files;
1043 	const uint32_t name_max = p->name_max;
1044 	struct stat st;
1045 	int count = -1;
1046 	while (++count < DIRLIST_BATCH && (dent = readdir(p->dp)) != NULL) {
1047 		const char * const d_name = dent->d_name;
1048 		const uint32_t dsz = (uint32_t) _D_EXACT_NAMLEN(dent);
1049 		if (d_name[0] == '.') {
1050 			if (hide_dotfiles)
1051 				continue;
1052 			if (d_name[1] == '\0')
1053 				continue;
1054 			if (d_name[1] == '.' && d_name[2] == '\0')
1055 				continue;
1056 		}
1057 
1058 		if (p->conf.hide_readme_file
1059 		    && p->conf.show_readme
1060 		    && buffer_eq_slen(p->conf.show_readme, d_name, dsz))
1061 			continue;
1062 		if (p->conf.hide_header_file
1063 		    && p->conf.show_header
1064 		    && buffer_eq_slen(p->conf.show_header, d_name, dsz))
1065 			continue;
1066 
1067 		/* compare d_name against excludes array
1068 		 * elements, skipping any that match.
1069 		 */
1070 		if (p->conf.excludes
1071 		    && mod_dirlisting_exclude(p->conf.excludes, d_name, dsz))
1072 			continue;
1073 
1074 		/* NOTE: the manual says, d_name is never more than NAME_MAX
1075 		 *       so this should actually not be a buffer-overflow-risk
1076 		 */
1077 		if (dsz > name_max) continue;
1078 	  #ifdef __COVERITY__
1079 		/* For some reason, Coverity overlooks the strlen() performed
1080 		 * a few lines above and thinks memcpy() below might access
1081 		 * bytes beyond end of d_name[] with dsz+1 */
1082 		force_assert(dsz < sizeof(dent->d_name));
1083 	  #endif
1084 
1085 	  #ifndef _ATFILE_SOURCE
1086 		memcpy(p->path_file, d_name, dsz + 1);
1087 		if (stat(p->path, &st) != 0)
1088 			continue;
1089 	  #else
1090 		/*(XXX: follow symlinks, like stat(); not using AT_SYMLINK_NOFOLLOW) */
1091 		if (0 != fstatat(p->dfd, d_name, &st, 0))
1092 			continue; /* file *just* disappeared? */
1093 	  #endif
1094 
1095 		if (p->jb) { /* json output */
1096 			if (__builtin_expect( (p->jcomma), 1))/*(to avoid excess comma)*/
1097 				buffer_append_string_len(p->jb, CONST_STR_LEN(",{\"name\":\""));
1098 			else {
1099 				p->jcomma = 1;
1100 				buffer_append_string_len(p->jb, CONST_STR_LEN( "{\"name\":\""));
1101 			}
1102 			buffer_append_bs_escaped_json(p->jb, d_name, dsz);
1103 
1104 			const char *t;
1105 			size_t tlen;
1106 			if (!S_ISDIR(st.st_mode)) {
1107 				t =           "\",\"type\":\"file\",\"size\":";
1108 				tlen = sizeof("\",\"type\":\"file\",\"size\":")-1;
1109 			}
1110 			else {
1111 				t =           "\",\"type\":\"dir\",\"size\":";
1112 				tlen = sizeof("\",\"type\":\"dir\",\"size\":")-1;
1113 			}
1114 			char sstr[LI_ITOSTRING_LENGTH];
1115 			char mstr[LI_ITOSTRING_LENGTH];
1116 			struct const_iovec iov[] = {
1117 			  { t, tlen }
1118 			 ,{ sstr, li_itostrn(sstr, sizeof(sstr), st.st_size) }
1119 			 ,{ CONST_STR_LEN(",\"mtime\":") }
1120 			 ,{ mstr, li_itostrn(mstr, sizeof(mstr), TIME64_CAST(st.st_mtime)) }
1121 			 ,{ CONST_STR_LEN("}") }
1122 			};
1123 			buffer_append_iovec(p->jb, iov, sizeof(iov)/sizeof(*iov));
1124 			continue;
1125 		}
1126 
1127 		dirls_list_t * const list = !S_ISDIR(st.st_mode) ? &p->files : &p->dirs;
1128 		if (list->used == list->size) {
1129 			list->size += DIRLIST_BLOB_SIZE;
1130 			list->ent   = (dirls_entry_t**) realloc(list->ent, sizeof(dirls_entry_t*) * list->size);
1131 			force_assert(list->ent);
1132 		}
1133 		dirls_entry_t * const tmp = list->ent[list->used++] =
1134 		  (dirls_entry_t*) malloc(sizeof(dirls_entry_t) + 1 + dsz);
1135 		force_assert(tmp);
1136 		tmp->mtime = st.st_mtime;
1137 		tmp->size  = st.st_size;
1138 		tmp->namelen = dsz;
1139 		memcpy(DIRLIST_ENT_NAME(tmp), d_name, dsz + 1);
1140 	}
1141 	if (count == DIRLIST_BATCH)
1142 		return HANDLER_WAIT_FOR_EVENT;
1143 	closedir(p->dp);
1144 	p->dp = NULL;
1145 
1146 	return HANDLER_FINISHED;
1147 }
1148 
1149 static void http_list_directory(request_st * const r, handler_ctx * const hctx) {
1150 	dirls_list_t * const dirs = &hctx->dirs;
1151 	dirls_list_t * const files = &hctx->files;
1152 	/*(note: sorting can be time consuming on large dirs (O(n log n))*/
1153 	if (dirs->used) http_dirls_sort(dirs->ent, dirs->used);
1154 	if (files->used) http_dirls_sort(files->ent, files->used);
1155 
1156 	char sizebuf[sizeof("999.9K")];
1157 	struct tm tm;
1158 
1159 	/* generate large directory listings into tempfiles
1160 	 * (estimate approx 200-256 bytes of HTML per item; could be up to ~512) */
1161 	chunkqueue * const cq = &r->write_queue;
1162 	buffer * const tb = r->tmp_buf;
1163 	buffer_clear(tb);
1164 	buffer * const out = (dirs->used + files->used <= 256)
1165 	  ? chunkqueue_append_buffer_open(cq)
1166 	  : tb;
1167 	buffer_clear(out);
1168 
1169 	/* directories */
1170 	dirls_entry_t ** const dirs_ent = dirs->ent;
1171 	for (uint32_t i = 0, used = dirs->used; i < used; ++i) {
1172 		dirls_entry_t * const tmp = dirs_ent[i];
1173 
1174 		buffer_append_string_len(out, CONST_STR_LEN("<tr class=\"d\"><td class=\"n\"><a href=\""));
1175 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1176 		buffer_append_string_len(out, CONST_STR_LEN("/\">"));
1177 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1178 		buffer_append_string_len(out, CONST_STR_LEN("</a>/</td><td class=\"m\">"));
1179 		buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1180 		buffer_append_string_len(out, CONST_STR_LEN("</td><td class=\"s\">- &nbsp;</td><td class=\"t\">Directory</td></tr>\n"));
1181 
1182 		if (buffer_string_space(out) < 256) {
1183 			if (out == tb) {
1184 				if (0 != chunkqueue_append_mem_to_tempfile(cq,
1185 				                                           BUF_PTR_LEN(out),
1186 				                                           r->conf.errh))
1187 					break;
1188 				buffer_clear(out);
1189 			}
1190 		}
1191 	}
1192 
1193 	/* files */
1194 	const array * const mimetypes = r->conf.mimetypes;
1195 	dirls_entry_t ** const files_ent = files->ent;
1196 	for (uint32_t i = 0, used = files->used; i < used; ++i) {
1197 		dirls_entry_t * const tmp = files_ent[i];
1198 		const buffer *content_type;
1199 	  #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR) /*(pass full path)*/
1200 		content_type = NULL;
1201 		if (r->conf.use_xattr) {
1202 			memcpy(hctx->path_file, DIRLIST_ENT_NAME(tmp), tmp->namelen + 1);
1203 			content_type = stat_cache_mimetype_by_xattr(hctx->path);
1204 		}
1205 		if (NULL == content_type)
1206 	  #endif
1207 			content_type = stat_cache_mimetype_by_ext(mimetypes, DIRLIST_ENT_NAME(tmp), tmp->namelen);
1208 		if (NULL == content_type) {
1209 			static const buffer octet_stream =
1210 			  { "application/octet-stream",
1211 			    sizeof("application/octet-stream"), 0 };
1212 			content_type = &octet_stream;
1213 		}
1214 
1215 		buffer_append_string_len(out, CONST_STR_LEN("<tr><td class=\"n\"><a href=\""));
1216 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_REL_URI_PART);
1217 		buffer_append_string_len(out, CONST_STR_LEN("\">"));
1218 		buffer_append_string_encoded(out, DIRLIST_ENT_NAME(tmp), tmp->namelen, ENCODING_MINIMAL_XML);
1219 		buffer_append_string_len(out, CONST_STR_LEN("</a></td><td class=\"m\">"));
1220 		buffer_append_strftime(out, "%Y-%b-%d %T", localtime64_r(&tmp->mtime, &tm));
1221 		size_t buflen =
1222 		  http_list_directory_sizefmt(sizebuf, sizeof(sizebuf), tmp->size);
1223 		struct const_iovec iov[] = {
1224 		  { CONST_STR_LEN("</td><td class=\"s\">") }
1225 		 ,{ sizebuf, buflen }
1226 		 ,{ CONST_STR_LEN("</td><td class=\"t\">") }
1227 		 ,{ BUF_PTR_LEN(content_type) }
1228 		 ,{ CONST_STR_LEN("</td></tr>\n") }
1229 		};
1230 		buffer_append_iovec(out, iov, sizeof(iov)/sizeof(*iov));
1231 
1232 		if (buffer_string_space(out) < 256) {
1233 			if (out == tb) {
1234 				if (0 != chunkqueue_append_mem_to_tempfile(cq,
1235 				                                           BUF_PTR_LEN(out),
1236 				                                           r->conf.errh))
1237 					break;
1238 				buffer_clear(out);
1239 			}
1240 		}
1241 	}
1242 
1243 	if (out == tb) {
1244 		if (!buffer_is_blank(out)
1245 		    && 0 != chunkqueue_append_mem_to_tempfile(cq,
1246 		                                              BUF_PTR_LEN(out),
1247 		                                              r->conf.errh)) {
1248 			/* ignore */
1249 		}
1250 	}
1251 	else {
1252 		chunkqueue_append_buffer_commit(cq);
1253 	}
1254 }
1255 
1256 
1257 static void mod_dirlisting_content_type (request_st * const r, const buffer * const encoding) {
1258     buffer * const vb =
1259       http_header_response_set_ptr(r, HTTP_HEADER_CONTENT_TYPE,
1260                                    CONST_STR_LEN("Content-Type"));
1261     if (NULL == encoding)
1262         buffer_copy_string_len(vb, CONST_STR_LEN("text/html"));
1263     else
1264         buffer_append_str2(vb, CONST_STR_LEN("text/html;charset="),
1265                                BUF_PTR_LEN(encoding));
1266 }
1267 
1268 
1269 static void mod_dirlisting_response (request_st * const r, handler_ctx * const hctx) {
1270     http_list_directory_header(r, hctx);
1271     http_list_directory(r, hctx);
1272     http_list_directory_footer(r, hctx);
1273     mod_dirlisting_content_type(r, hctx->conf.encoding);
1274 
1275     r->resp_body_finished = 1;
1276 }
1277 
1278 
1279 static void mod_dirlisting_json_append (request_st * const r, handler_ctx * const hctx, const int fin) {
1280     buffer * const jb = hctx->jb;
1281     if (fin)
1282         buffer_append_string_len(jb, CONST_STR_LEN("]}"));
1283     else if (buffer_clen(jb) < 16384-1024)
1284         return; /* aggregate bunches of entries, even if streaming response */
1285 
1286     if (hctx->jfn) {
1287         if (__builtin_expect( (write_all(hctx->jfd, BUF_PTR_LEN(jb)) < 0), 0)) {
1288             /*(cleanup, cease caching if error occurs writing to cache file)*/
1289             unlink(hctx->jfn);
1290             free(hctx->jfn);
1291             hctx->jfn = NULL;
1292             close(hctx->jfd);
1293             hctx->jfd = -1;
1294         }
1295         /* Note: writing cache file is separate from the response so that if an
1296          * error occurs with cache, the response still proceeds.  While this is
1297          * duplicative if the response is large enough to spill to temporary
1298          * files, it is expected that only very large directories will spill to
1299          * temporary files, and even then most responses will be less than 1 MB.
1300          * The cache path can be different from server.upload-dirs.
1301          * Note: since responses are not expected to be large, no effort is
1302          * currently made here to handle FDEVENT_STREAM_RESPONSE_BUFMIN and to
1303          * defer reading more from directory while data is sent to client */
1304     }
1305 
1306     http_chunk_append_buffer(r, jb); /* clears jb */
1307 }
1308 
1309 
1310 SUBREQUEST_FUNC(mod_dirlisting_subrequest);
1311 REQUEST_FUNC(mod_dirlisting_reset);
1312 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p);
1313 __attribute_noinline__
1314 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx);
1315 __attribute_noinline__
1316 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx);
1317 __attribute_noinline__
1318 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx);
1319 
1320 
1321 URIHANDLER_FUNC(mod_dirlisting_subrequest_start) {
1322 	plugin_data *p = p_d;
1323 
1324 	if (NULL != r->handler_module) return HANDLER_GO_ON;
1325 	if (!buffer_has_slash_suffix(&r->uri.path)) return HANDLER_GO_ON;
1326 	if (!http_method_get_or_head(r->http_method)) return HANDLER_GO_ON;
1327 	/* r->physical.path is non-empty for handle_subrequest_start */
1328 	/*if (buffer_is_blank(&r->physical.path)) return HANDLER_GO_ON;*/
1329 
1330 	mod_dirlisting_patch_config(r, p);
1331 
1332 	if (!p->conf.dir_listing) return HANDLER_GO_ON;
1333 
1334 	if (r->conf.log_request_handling) {
1335 		log_error(r->conf.errh, __FILE__, __LINE__,
1336 		  "-- handling the request as Dir-Listing");
1337 		log_error(r->conf.errh, __FILE__, __LINE__,
1338 		  "URI          : %s", r->uri.path.ptr);
1339 	}
1340 
1341   #if 0 /* redundant check; not necessary */
1342 	/* r->physical.path is a dir since it ends in slash, or else
1343 	 * http_response_physical_path_check() would have redirected
1344 	 * before calling handle_subrequest_start */
1345 	if (!stat_cache_path_isdir(&r->physical.path)) {
1346 		if (errno == ENOTDIR)
1347 			return HANDLER_GO_ON;
1348 		log_perror(r->conf.errh,__FILE__,__LINE__,"%s",r->physical.path.ptr);
1349 		r->http_status = 500;
1350 		return HANDLER_FINISHED;
1351 	}
1352   #endif
1353 
1354 	/* TODO: add option/mechanism to enable json output */
1355   #if 0 /* XXX: ??? might this be enabled accidentally by clients ??? */
1356 	const buffer * const vb =
1357 	  http_header_request_get(r, HTTP_HEADER_ACCEPT, CONST_STR_LEN("Accept"));
1358 	p->conf.json = (vb && strstr(vb->ptr, "application/json")); /*(coarse)*/
1359   #endif
1360 
1361 	if (p->conf.cache) {
1362 		handler_t rc = mod_dirlisting_cache_check(r, p);
1363 		if (rc != HANDLER_GO_ON)
1364 			return rc;
1365 	}
1366 
1367 	/* upper limit for dirlisting requests in progress (per lighttpd worker)
1368 	 * (attempt to avoid "livelock" scenarios or starvation of other requests)
1369 	 * (100 is still a high arbitrary limit;
1370 	 *  and limit applies only to directories larger than DIRLIST_BATCH-2) */
1371 	if (p->processing == dirlist_max_in_progress) {
1372 		r->http_status = 503;
1373 		http_header_response_set(r, HTTP_HEADER_OTHER,
1374 		                         CONST_STR_LEN("Retry-After"),
1375 		                         CONST_STR_LEN("2"));
1376 		return HANDLER_FINISHED;
1377 	}
1378 
1379 	handler_ctx * const hctx = mod_dirlisting_handler_ctx_init(p);
1380 
1381 	/* future: might implement a queue to limit max number of dirlisting
1382 	 * requests being serviced in parallel (increasing disk I/O), and if
1383 	 * caching is enabled, to avoid repeating the work on the same directory
1384 	 * in parallel.  Could continue serving (expired) cached entry while
1385 	 * updating, but burst of requests on first access to dir would still
1386 	 * need to be handled.
1387 	 *
1388 	 * If queueing (not implemented), defer opening dir until pulled off
1389 	 * queue.  Since joblist is per-connection, would need to handle single
1390 	 * request from queue even if multiple streams are queued on same
1391 	 * HTTP/2 connection.  If queueing, must check for and remove from
1392 	 * queue in mod_dirlisting_reset() if request is still queued. */
1393 
1394 	if (0 != http_open_directory(r, hctx)) {
1395 		/* dirlisting failed */
1396 		r->http_status = 403;
1397 	        mod_dirlisting_handler_ctx_free(hctx);
1398 		return HANDLER_FINISHED;
1399 	}
1400 	++p->processing;
1401 
1402 	if (p->conf.json) {
1403 		hctx->jfd = -1;
1404 		hctx->jb = chunk_buffer_acquire();
1405 		buffer_append_string_len(hctx->jb, CONST_STR_LEN("{["));
1406 		if (p->conf.cache)
1407 			mod_dirlisting_cache_json_init(r, hctx);
1408 		http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1409 		                         CONST_STR_LEN("Content-Type"),
1410 		                         CONST_STR_LEN("application/json"));
1411 		r->http_status = 200;
1412 		r->resp_body_started = 1;
1413 	}
1414 
1415 	r->plugin_ctx[p->id] = hctx;
1416 	r->handler_module = p->self;
1417 	return mod_dirlisting_subrequest(r, p);
1418 }
1419 
1420 
1421 SUBREQUEST_FUNC(mod_dirlisting_subrequest) {
1422     plugin_data * const p = p_d;
1423     handler_ctx * const hctx = r->plugin_ctx[p->id];
1424     if (NULL == hctx) return HANDLER_GO_ON; /*(should not happen)*/
1425 
1426     handler_t rc = http_read_directory(hctx);
1427     switch (rc) {
1428       case HANDLER_FINISHED:
1429         if (hctx->jb) { /* (hctx->conf.json) */
1430             mod_dirlisting_json_append(r, hctx, 1);
1431             r->resp_body_finished = 1;
1432             if (hctx->jfn) /* (also (hctx->conf.cache) */
1433                 mod_dirlisting_cache_json(r, hctx);
1434         }
1435         else {
1436             mod_dirlisting_response(r, hctx);
1437             if (hctx->conf.cache)
1438                 mod_dirlisting_cache_add(r, hctx);
1439         }
1440         mod_dirlisting_reset(r, p); /*(release resources, including hctx)*/
1441         break;
1442       case HANDLER_WAIT_FOR_EVENT: /*(used here to mean 'yield')*/
1443         if (hctx->jb)   /* (hctx->conf.json) */
1444             mod_dirlisting_json_append(r, hctx, 0);
1445         joblist_append(r->con);
1446         break;
1447       default:
1448         break;
1449     }
1450 
1451     return rc;
1452 }
1453 
1454 
1455 REQUEST_FUNC(mod_dirlisting_reset) {
1456     void ** const restrict dptr = &r->plugin_ctx[((plugin_data *)p_d)->id];
1457     if (*dptr) {
1458         --((plugin_data *)p_d)->processing;
1459         mod_dirlisting_handler_ctx_free(*dptr);
1460         *dptr = NULL;
1461     }
1462     return HANDLER_GO_ON;
1463 }
1464 
1465 
1466 static void mod_dirlisting_cache_control (request_st * const r, unix_time64_t max_age) {
1467     if (!light_btst(r->resp_htags, HTTP_HEADER_CACHE_CONTROL)) {
1468         buffer * const vb =
1469           http_header_response_set_ptr(r, HTTP_HEADER_CACHE_CONTROL,
1470                                        CONST_STR_LEN("Cache-Control"));
1471         buffer_append_string_len(vb, CONST_STR_LEN("max-age="));
1472         buffer_append_int(vb, max_age);
1473     }
1474 }
1475 
1476 
1477 static handler_t mod_dirlisting_cache_check (request_st * const r, plugin_data * const p) {
1478     /* optional: an external process can trigger a refresh by deleting the cache
1479      * entry when the external process detects (or initiates) changes to dir */
1480     buffer * const tb = r->tmp_buf;
1481     buffer_copy_path_len2(tb, BUF_PTR_LEN(p->conf.cache->path),
1482                               BUF_PTR_LEN(&r->physical.path));
1483     buffer_append_string_len(tb, p->conf.json ? "dirlist.json" : "dirlist.html",
1484                              sizeof("dirlist.html")-1);
1485     stat_cache_entry * const sce = stat_cache_get_entry_open(tb, 1);
1486     if (NULL == sce || sce->fd == -1)
1487         return HANDLER_GO_ON;
1488     const unix_time64_t max_age =
1489       TIME64_CAST(sce->st.st_mtime) + p->conf.cache->max_age - log_epoch_secs;
1490     if (max_age < 0)
1491         return HANDLER_GO_ON;
1492 
1493     !p->conf.json
1494       ? mod_dirlisting_content_type(r, p->conf.encoding)
1495       : http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE,
1496                                  CONST_STR_LEN("Content-Type"),
1497                                  CONST_STR_LEN("application/json"));
1498 
1499   #if 0
1500     /*(XXX: ETag needs to be set for mod_deflate to potentially handle)*/
1501     /*(XXX: should ETag be created from cache file mtime or directory mtime?)*/
1502     const int follow_symlink = r->conf.follow_symlink;
1503     r->conf.follow_symlink = 1; /*(skip symlink checks into cache)*/
1504     http_response_send_file(r, sce->name, sce);
1505     r->conf.follow_symlink = follow_symlink;
1506     if (r->http_status < 400)
1507         return HANDLER_FINISHED;
1508     r->http_status = 0;
1509   #endif
1510 
1511     /* Note: dirlist < 350 or so entries will generally trigger file
1512      * read into memory for dirlist < 32k, which will not be able to use
1513      * mod_deflate cache.  Still, this is much more efficient than lots of
1514      * stat() calls to generate the dirlisting for each and every request */
1515     if (0 != http_chunk_append_file_ref(r, sce)) {
1516         http_header_response_unset(r, HTTP_HEADER_CONTENT_TYPE,
1517                                    CONST_STR_LEN("Content-Type"));
1518         http_response_body_clear(r, 0);
1519         return HANDLER_GO_ON;
1520     }
1521 
1522     /* Cache-Control and ETag (also done in mod_dirlisting_cache_add())*/
1523     mod_dirlisting_cache_control(r, max_age);
1524     if (0 != r->conf.etag_flags) {
1525         const buffer *etag = stat_cache_etag_get(sce, r->conf.etag_flags);
1526         if (etag && !buffer_is_blank(etag))
1527             http_header_response_set(r, HTTP_HEADER_ETAG,
1528                                      CONST_STR_LEN("ETag"),
1529                                      BUF_PTR_LEN(etag));
1530     }
1531 
1532     r->resp_body_finished = 1;
1533     return HANDLER_FINISHED;
1534 }
1535 
1536 
1537 static int mod_dirlisting_write_cq (const int fd, chunkqueue * const cq, log_error_st * const errh)
1538 {
1539     chunkqueue in;
1540     memset(&in, 0, sizeof(in));
1541     chunkqueue_append_chunkqueue(&in, cq);
1542     cq->bytes_in  -= in.bytes_in;
1543     cq->bytes_out -= in.bytes_in;
1544 
1545     /*(similar to mod_webdav.c:mod_webdav_write_cq(), but operates on two cqs)*/
1546     while (!chunkqueue_is_empty(&in)) {
1547         ssize_t wr = chunkqueue_write_chunk(fd, &in, errh);
1548         if (__builtin_expect( (wr > 0), 1))
1549             chunkqueue_steal(cq, &in, wr);
1550         else if (wr < 0) {
1551             /*(writing to tempfile failed; transfer remaining data back to cq)*/
1552             chunkqueue_append_chunkqueue(cq, &in);
1553             return 0;
1554         }
1555         else /*(wr == 0)*/
1556             chunkqueue_remove_finished_chunks(&in);
1557     }
1558     return 1;
1559 }
1560 
1561 
1562 #include <sys/stat.h>   /* mkdir() */
1563 #include <sys/types.h>
1564 /*(similar to mod_deflate.c:mkdir_recursive(), but starts mid-path)*/
1565 static int mkdir_recursive (char *dir, size_t off) {
1566     char *p = dir+off;
1567     if (*p != '/') {
1568         if (off && p[-1] == '/')
1569             --p;
1570         else {
1571             errno = ENOTDIR;
1572             return -1;
1573         }
1574     }
1575     do {
1576         *p = '\0';
1577         int rc = mkdir(dir, 0700);
1578         *p = '/';
1579         if (0 != rc && errno != EEXIST) return -1;
1580     } while ((p = strchr(p+1, '/')) != NULL);
1581     return 0;
1582 }
1583 
1584 
1585 #include <stdio.h>      /* rename() */
1586 __attribute_noinline__
1587 static void mod_dirlisting_cache_add (request_st * const r, handler_ctx * const hctx) {
1588   #ifndef PATH_MAX
1589   #define PATH_MAX 4096
1590   #endif
1591     char oldpath[PATH_MAX];
1592     char newpath[PATH_MAX];
1593     buffer * const tb = r->tmp_buf;
1594     buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1595                               BUF_PTR_LEN(&r->physical.path));
1596     if (!stat_cache_path_isdir(tb)
1597         && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1598         return;
1599     buffer_append_string_len(tb, CONST_STR_LEN("dirlist.html"));
1600     const size_t len = buffer_clen(tb);
1601     if (len + 7 >= PATH_MAX) return;
1602     memcpy(newpath, tb->ptr, len+1);   /*(include '\0')*/
1603     buffer_append_string_len(tb, CONST_STR_LEN(".XXXXXX"));
1604     memcpy(oldpath, tb->ptr, len+7+1); /*(include '\0')*/
1605     const int fd = fdevent_mkostemp(oldpath, 0);
1606     if (fd < 0) return;
1607     if (mod_dirlisting_write_cq(fd, &r->write_queue, r->conf.errh)
1608         && 0 == rename(oldpath, newpath)) {
1609         stat_cache_invalidate_entry(newpath, len);
1610         /* Cache-Control and ETag (also done in mod_dirlisting_cache_check())*/
1611         mod_dirlisting_cache_control(r, hctx->conf.cache->max_age);
1612         if (0 != r->conf.etag_flags) {
1613             struct stat st;
1614             if (0 == fstat(fd, &st)) {
1615                 buffer * const vb =
1616                   http_header_response_set_ptr(r, HTTP_HEADER_ETAG,
1617                                                CONST_STR_LEN("ETag"));
1618                 http_etag_create(vb, &st, r->conf.etag_flags);
1619             }
1620         }
1621     }
1622     else
1623         unlink(oldpath);
1624     close(fd);
1625 }
1626 
1627 
1628 __attribute_noinline__
1629 static void mod_dirlisting_cache_json_init (request_st * const r, handler_ctx * const hctx) {
1630   #ifndef PATH_MAX
1631   #define PATH_MAX 4096
1632   #endif
1633     buffer * const tb = r->tmp_buf;
1634     buffer_copy_path_len2(tb, BUF_PTR_LEN(hctx->conf.cache->path),
1635                               BUF_PTR_LEN(&r->physical.path));
1636     if (!stat_cache_path_isdir(tb)
1637         && 0 != mkdir_recursive(tb->ptr, buffer_clen(hctx->conf.cache->path)))
1638         return;
1639     buffer_append_string_len(tb, CONST_STR_LEN("dirlist.json.XXXXXX"));
1640     const int fd = fdevent_mkostemp(tb->ptr, 0);
1641     if (fd < 0) return;
1642     hctx->jfn_len = buffer_clen(tb);
1643     hctx->jfd = fd;
1644     hctx->jfn = malloc(hctx->jfn_len+1);
1645     force_assert(hctx->jfn);
1646     memcpy(hctx->jfn, tb->ptr, hctx->jfn_len+1); /*(include '\0')*/
1647 }
1648 
1649 
1650 __attribute_noinline__
1651 static void mod_dirlisting_cache_json (request_st * const r, handler_ctx * const hctx) {
1652   #ifndef PATH_MAX
1653   #define PATH_MAX 4096
1654   #endif
1655     UNUSED(r);
1656     char newpath[PATH_MAX];
1657     const size_t len = hctx->jfn_len - 7; /*(-7 for .XXXXXX)*/
1658     force_assert(len < PATH_MAX);
1659     memcpy(newpath, hctx->jfn, len);
1660     newpath[len] = '\0';
1661     if (0 == rename(hctx->jfn, newpath))
1662         stat_cache_invalidate_entry(newpath, len);
1663     else
1664         unlink(hctx->jfn);
1665     free(hctx->jfn);
1666     hctx->jfn = NULL;
1667 }
1668 
1669 
1670 __attribute_cold__
1671 int mod_dirlisting_plugin_init(plugin *p);
1672 int mod_dirlisting_plugin_init(plugin *p) {
1673 	p->version     = LIGHTTPD_VERSION_ID;
1674 	p->name        = "dirlisting";
1675 
1676 	p->init        = mod_dirlisting_init;
1677 	p->handle_subrequest_start = mod_dirlisting_subrequest_start;
1678 	p->handle_subrequest       = mod_dirlisting_subrequest;
1679 	p->handle_request_reset    = mod_dirlisting_reset;
1680 	p->set_defaults  = mod_dirlisting_set_defaults;
1681 	p->cleanup     = mod_dirlisting_free;
1682 
1683 	return 0;
1684 }
1685