32c6de9fb716e4d8e0223b6a7af7cc921bb4a191
[gnupg-doc.git] / tools / ftp-indexer.c
1 /* ftp-indexer.c - Create an HTML index file for an FTP directory
2  * Copyright (C) 2017  g10 Code GmbH
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, see <https://www.gnu.org/licenses/>.
16  */
17
18 /* The following script is triggred by a cronjob at ftp.gnupg.org to
19  * build the index pages.
20  *
21 --8<---------------cut here---------------start------------->8---
22 #!/bin/sh
23
24 set -e
25 top=/home/ftp
26 scratch=/home/ftp/.scratch
27 cd "$top"
28
29 opt_force=no
30 if [ "$1" == "--force" ]; then
31   shift
32   opt_force=yes
33 fi
34
35 INDEXER=/usr/local/bin/ftp-indexer
36 if [ ! -x $INDEXER ]; then
37   echo "mk-ftp-index.html.sh: Index tool $INDEXER not found - aborting" >&2
38   exit 1
39 fi
40 INDEXER_OPTS="--reverse-ver --gpgweb --readme --index $scratch/ftp-index.new"
41 INDEXER_OPTS="$INDEXER_OPTS --exclude README --exclude index.html"
42
43
44 (find . -type d ! -name '\.*' ! -name dev ; echo .) |\
45  while read dir rest; do
46   dir=${dir##./}
47   if cd "$dir"; then
48     if [ "$dir" = "." ]; then
49       desc="/"
50       extraopt="--exclude dev"
51     else
52       desc="$dir/"
53       extraopt=""
54     fi
55
56     [ -f $scratch/index.html ] && rm $scratch/index.html
57     [ -f index.html ] && cat index.html >$scratch/index.html
58     $INDEXER $INDEXER_OPTS $extraopt . "$desc" >$scratch/index.html.new
59     if [ $opt_force = no -a -f $scratch/index.html ]; then
60       grep -v '^<meta name="date"' $scratch/index.html \
61           | grep -v '^Page last updated on ' >$scratch/index.html.x
62       grep -v '^<meta name="date"' $scratch/index.html.new \
63           | grep -v '^Page last updated on ' >$scratch/index.html.new.x
64       if ! cmp -s $scratch/index.html.x $scratch/index.html.new.x ; then
65          mv $scratch/index.html.new index.html
66          mv $scratch/ftp-index.new .ftp-index
67       fi
68       rm $scratch/index.html
69       [ -f $scratch/index.html.new ] && rm $scratch/index.html.new
70       [ -f $scratch/ftp-index.new ] && rm $scratch/ftp-index.new
71     else
72       mv $scratch/index.html.new index.html
73       mv $scratch/ftp-index.new .ftp-index
74     fi
75   fi
76   cd "$top"
77 done
78 --8<---------------cut here---------------end--------------->8---
79  *
80  **/
81
82 #include <stdio.h>
83 #include <stdlib.h>
84 #include <string.h>
85 #include <stdarg.h>
86 #include <ctype.h>
87 #include <sys/types.h>
88 #include <sys/stat.h>
89 #include <unistd.h>
90 #include <dirent.h>
91 #include <time.h>
92 #include <errno.h>
93
94
95 #define PGMNAME "ftp-indexer"
96 #define VERSION "0.1"
97
98 #define DIM(v)               (sizeof(v)/sizeof((v)[0]))
99 #define DIMof(type,member)   DIM(((type *)0)->member)
100 #if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 5)
101 #define ATTR_PRINTF(a,b)    __attribute__ ((format (printf,a,b)))
102 #define ATTR_NR_PRINTF(a,b) __attribute__ ((noreturn,format (printf,a,b)))
103 #else
104 #define ATTR_PRINTF(a,b)
105 #define ATTR_NR_PRINTF(a,b)
106 #endif
107
108 #define digitp(a) ((a) >= '0' && (a) <= '9')
109 #define VALID_URI_CHARS "abcdefghijklmnopqrstuvwxyz"   \
110                         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"   \
111                         "01234567890@"                 \
112                         "!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~"
113
114
115 /* A simple object to keep strings in a list.  */
116 struct strlist_s
117 {
118   struct strlist_s *next;
119   char d[1];
120 };
121 typedef struct strlist_s *strlist_t;
122
123
124 /* An object to collect information about files.  */
125 struct finfo_s
126 {
127   struct finfo_s *next;
128   unsigned int is_dir:1;
129   unsigned int is_reg:1;
130   time_t mtime;
131   unsigned long long size;
132   char name[1];
133 };
134 typedef struct finfo_s *finfo_t;
135
136
137 static int opt_verbose;
138 static int opt_debug;
139 static int opt_reverse;
140 static int opt_reverse_ver;
141 static int opt_files_first;
142 static int opt_html;
143 static const char *opt_index;
144 static int opt_gpgweb;
145 static int opt_readme;
146 static strlist_t opt_exclude;
147
148 static void die (const char *format, ...) ATTR_NR_PRINTF(1,2);
149 static void err (const char *format, ...) ATTR_PRINTF(1,2);
150 static void inf (const char *format, ...) ATTR_PRINTF(1,2);
151
152
153
154 static void
155 die (const char *fmt, ...)
156 {
157   va_list arg_ptr;
158
159   va_start (arg_ptr, fmt);
160   fputs (PGMNAME": ", stderr);
161   vfprintf (stderr, fmt, arg_ptr);
162   va_end (arg_ptr);
163   exit (1);
164 }
165
166
167 static void
168 err (const char *fmt, ...)
169 {
170   va_list arg_ptr;
171
172   va_start (arg_ptr, fmt);
173   fputs (PGMNAME": ", stderr);
174   vfprintf (stderr, fmt, arg_ptr);
175   va_end (arg_ptr);
176 }
177
178
179 static void
180 inf (const char *fmt, ...)
181 {
182   va_list arg_ptr;
183
184   if (!opt_verbose)
185     return;
186
187   va_start (arg_ptr, fmt);
188   fputs (PGMNAME": ", stderr);
189   vfprintf (stderr, fmt, arg_ptr);
190   va_end (arg_ptr);
191 }
192
193
194 static void *
195 xmalloc (size_t n)
196 {
197   void *p = malloc (n);
198   if (!p)
199     die ("out of core\n");
200   return p;
201 }
202
203
204 static void *
205 xcalloc (size_t n, size_t k)
206 {
207   void *p = calloc (n, k);
208   if (!p)
209     die ("out of core\n");
210   return p;
211 }
212
213
214 static char *
215 xstrdup (const char *string)
216 {
217   char *buf = xmalloc (strlen (string));
218   strcpy (buf, string);
219   return buf;
220 }
221
222
223 static inline char *
224 my_stpcpy (char *a, const char *b)
225 {
226   while (*b)
227     *a++ = *b++;
228   *a = 0;
229
230   return (char*)a;
231 }
232
233
234 /* If SPECIAL is NULL this function escapes in forms mode.  */
235 static size_t
236 escape_data (char *buffer, const void *data, size_t datalen,
237              const char *special)
238 {
239   int forms = !special;
240   const unsigned char *s;
241   size_t n = 0;
242
243   if (forms)
244     special = "%;?&=";
245
246   for (s = data; datalen; s++, datalen--)
247     {
248       if (forms && *s == ' ')
249         {
250           if (buffer)
251             *buffer++ = '+';
252           n++;
253         }
254       else if (forms && *s == '\n')
255         {
256           if (buffer)
257             memcpy (buffer, "%0D%0A", 6);
258           n += 6;
259         }
260       else if (forms && *s == '\r' && datalen > 1 && s[1] == '\n')
261         {
262           if (buffer)
263             memcpy (buffer, "%0D%0A", 6);
264           n += 6;
265           s++;
266           datalen--;
267         }
268       else if (strchr (VALID_URI_CHARS, *s) && !strchr (special, *s))
269         {
270           if (buffer)
271             *(unsigned char*)buffer++ = *s;
272           n++;
273         }
274       else
275         {
276           if (buffer)
277             {
278               snprintf (buffer, 4, "%%%02X", *s);
279               buffer += 3;
280             }
281           n += 3;
282         }
283     }
284   return n;
285 }
286
287
288 static int
289 insert_escapes (char *buffer, const char *string,
290                 const char *special)
291 {
292   return escape_data (buffer, string, strlen (string), special);
293 }
294
295
296 /* Allocate a new string from STRING using standard HTTP escaping as
297  * well as escaping of characters given in SPECIALS.  A common pattern
298  * for SPECIALS is "%;?&=". However it depends on the needs, for
299  * example "+" and "/: often needs to be escaped too.  Returns NULL on
300  * failure and sets ERRNO.  If SPECIAL is NULL a dedicated forms
301  * encoding mode is used.  */
302 static char *
303 http_escape_string (const char *string, const char *specials)
304 {
305   int n;
306   char *buf;
307
308   n = insert_escapes (NULL, string, specials);
309   buf = xmalloc (n+1);
310   insert_escapes (buf, string, specials);
311   buf[n] = 0;
312   return buf;
313 }
314
315
316 /* Same as http_escape_string but with an explict length.  */
317 static char *
318 http_escape_buffer (const char *string, size_t length, const char *specials)
319 {
320   int n;
321   char *buf;
322
323   n = escape_data (NULL, string, length, specials);
324   buf = xmalloc (n+1);
325   escape_data (buf, string, length, specials);
326   buf[n] = 0;
327   return buf;
328 }
329
330
331 /* Percent-escape the string STR by replacing colons with '%3a'.   */
332 static char *
333 do_percent_escape (const char *str)
334 {
335   int i, j;
336   char *ptr;
337
338   for (i=j=0; str[i]; i++)
339     if (str[i] == ':' || str[i] == '%' || str[i] == '\n')
340       j++;
341   ptr = xmalloc (i + 2 * j + 1);
342   i = 0;
343   while (*str)
344     {
345       if (*str == ':')
346         {
347           ptr[i++] = '%';
348           ptr[i++] = '3';
349           ptr[i++] = 'a';
350         }
351       else if (*str == '%')
352         {
353           ptr[i++] = '%';
354           ptr[i++] = '2';
355           ptr[i++] = '5';
356         }
357       else if (*str == '\n')
358         {
359           /* The newline is problematic in a line-based format.  */
360           ptr[i++] = '%';
361           ptr[i++] = '0';
362           ptr[i++] = 'a';
363         }
364       else
365         ptr[i++] = *str;
366       str++;
367     }
368   ptr[i] = '\0';
369
370   return ptr;
371 }
372
373
374 /* Simple percent escape for colon based listings.  Returns a
375  * statically allocated buffer.  */
376 static char *
377 percent_escape (const char *str)
378 {
379   static char *buffer;
380
381   free (buffer);
382   buffer = do_percent_escape (str);
383   return buffer;
384 }
385
386
387 /* Escape STRING at a max length of N for use in HTML.  Returns a
388  * statically allocated buffer.  */
389 static const char *
390 html_escape_n (const char *string, size_t length)
391 {
392   static char *buffer;
393   char *p;
394
395   /* The escaped string may be up to 6 times of STRING due to the
396      expansion of '\"' to "&quot;".  */
397   free (buffer);
398   p = buffer = xmalloc (6 * length + 1);
399   for (; *string && length; string++, length--)
400     {
401       switch (*string)
402         {
403         case '\"': p = my_stpcpy (p, "&quot;"); break;
404         case '&':  p = my_stpcpy (p, "&amp;"); break;
405         case '<':  p = my_stpcpy (p, "&lt;"); break;
406         case '>':  p = my_stpcpy (p, "&gt;"); break;
407         default:   *p++ = *string; break;
408         }
409     }
410   *p = 0;
411   return buffer;
412 }
413
414
415 /* Escape STRING for use in HTML.  Returns a statically allocated
416  * buffer.  noet that this buffer is shared with the buffer returned
417  * by html_escape_n.  */
418 static const char *
419 html_escape (const char *string)
420 {
421   return html_escape_n (string, strlen (string));
422 }
423
424
425 /* Escape STRING but insert <a> for one https link.  */
426 static const char *
427 html_escape_detect_link (const char *string)
428 {
429   const char *start, *s;
430   char *part1, *url, *part2, *part3;
431   size_t urllen;
432   char *buffer, *p;
433
434   start = strstr (string, "https://");
435   if (!start || !start[8] || start[8] == ' ' || start[8] == '\t')
436     return html_escape (string);
437   if (!(start == string || start[-1] == ' ' || start[-1] == '\t'
438         || start[-1] == '<'))
439     return html_escape (string);
440
441   urllen = 0;
442   for (s = start; *s && *s != ' ' && *s != '\t' && *s != '>'; s++)
443     urllen++;
444
445   part1 = xstrdup (html_escape_n (string, start-string));
446   url = http_escape_buffer (start, urllen, "\"");
447   part2 = xstrdup (html_escape_n (start, urllen));
448   part3 = xstrdup (html_escape (start + urllen));
449
450   buffer = xmalloc (strlen (part1) + strlen (url)
451                     + strlen (part2) + strlen (part3) + 100);
452   p = my_stpcpy (buffer, part1);
453   p = my_stpcpy (p, "<a href=\"");
454   p = my_stpcpy (p, url);
455   p = my_stpcpy (p, "\">");
456   p = my_stpcpy (p, part2);
457   p = my_stpcpy (p, "</a>");
458   my_stpcpy (p, part3);
459
460   free (part1);
461   free (url);
462   free (part2);
463   free (part3);
464   return buffer;
465 }
466
467
468
469 /* Escape STRING for use as a HREF attribute.  Returns a statically
470  * allocated buffer.  */
471 static const char *
472 html_escape_href (const char *string)
473 {
474   static char *buffer;
475
476   free (buffer);
477   buffer = http_escape_string (string, "\"");
478   return buffer;
479 }
480
481
482 /* Format T and return a statically allocated buffer.  */
483 static const char *
484 format_time_now (int human)
485 {
486   static char buffer[40];
487   struct tm *tp;
488   time_t now;
489
490   time (&now);
491   tp = gmtime (&now);
492   if (!tp)
493     *buffer = 0;
494
495   if (human)
496     snprintf (buffer, sizeof buffer, "%04d-%02d-%02d",
497               1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday);
498   else
499     snprintf (buffer, sizeof buffer, "%04d%02d%02dT%02d%02d%02dZ",
500               1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday,
501               tp->tm_hour, tp->tm_min, tp->tm_sec);
502
503   return buffer;
504 }
505
506
507 /* Format T and return a statically allocated buffer.  */
508 static const char *
509 format_time (time_t t)
510 {
511   static char buffer[80];
512   struct tm *tp;
513
514   tp = gmtime (&t);
515   if (!tp)
516     *buffer = 0;
517   else if (opt_html)
518     snprintf (buffer, sizeof buffer,
519               "<abbr title=\"%04d-%02d-%02d&nbsp;%02d:%02d:%02d UTC\">"
520               "%04d-%02d-%02d</abbr>",
521               1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday,
522               tp->tm_hour, tp->tm_min, tp->tm_sec,
523               1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday);
524   else
525     snprintf (buffer, sizeof buffer, "%04d%02d%02dT%02d%02d%02dZ",
526               1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday,
527               tp->tm_hour, tp->tm_min, tp->tm_sec);
528   return buffer;
529 }
530
531
532 /* Format SIZE and return a statically allocated buffer.  */
533 static const char *
534 format_size (unsigned long long size)
535 {
536   static char buffer[80];
537   const char *suffix;
538   unsigned long long val = size;
539
540   if (size < 1024)
541     {
542       val = size;
543       suffix = "";
544     }
545   else if (size < 1024 * 1024)
546     {
547       val = size / 1024;
548       suffix = "k";
549     }
550   else if (size < 1024 * 1024 * 1024)
551     {
552       val = size / (1024 * 1024);
553       suffix = "M";
554     }
555   else
556     {
557       val = size / (1024 * 1024 * 1024);
558       suffix = "G";
559     }
560
561   if (opt_html)
562     snprintf (buffer, sizeof buffer,
563               "<abbr title=\"%llu byte%s\">%llu%s</abbr>",
564               size, size == 1? "":"s",
565               val, suffix);
566   else
567     snprintf (buffer, sizeof buffer, "%llu%s", val, suffix);
568
569   return buffer;
570 }
571
572
573 /* This function parses the first portion of the version number S and
574  * stores it at NUMBER.  On success, this function returns a pointer
575  * into S starting with the first character, which is not part of the
576  * initial number portion; on failure, NULL is returned.  */
577 static const char*
578 parse_version_number (const char *s, int *number)
579 {
580   int val = 0;
581
582   if (*s == '0' && digitp (s[1]))
583     return NULL;  /* Leading zeros are not allowed.  */
584   for (; digitp (*s); s++)
585     {
586       val *= 10;
587       val += *s - '0';
588     }
589   *number = val;
590   return val < 0 ? NULL : s;
591 }
592
593
594 /* This function breaks up the complete string-representation of the
595  * version number S, which is of the following struture: <major
596  * number>.<minor number>.<micro number><patch level>.  The major,
597  * minor and micro number components will be stored in *MAJOR, *MINOR
598  * and *MICRO.
599  *
600  * On success, the last component, the patch level, will be returned;
601  * in failure, NULL will be returned.  */
602 static const char *
603 parse_version_string (const char *s, int *major, int *minor, int *micro)
604 {
605   s = parse_version_number (s, major);
606   if (!s || *s != '.')
607     return NULL;
608   s++;
609   s = parse_version_number (s, minor);
610   if (!s || *s != '.')
611     return NULL;
612   s++;
613   s = parse_version_number (s, micro);
614   if (!s)
615     return NULL;
616   return s; /* patchlevel */
617 }
618
619
620 /* Compare function for version strings.  */
621 static int
622 compare_version_strings (const char *a, const char *b)
623 {
624   int a_major, a_minor, a_micro;
625   int b_major, b_minor, b_micro;
626   const char *a_plvl, *b_plvl;
627
628   a_plvl = parse_version_string (a, &a_major, &a_minor, &a_micro);
629   if (!a_plvl)
630     a_major = a_minor = a_micro = 0;
631
632   b_plvl = parse_version_string (b, &b_major, &b_minor, &b_micro);
633   if (!b_plvl)
634     b_major = b_minor = b_micro = 0;
635
636   if (!a_plvl && !b_plvl)
637     return -1;  /* Put invalid strings at the end.  */
638   if (a_plvl && !b_plvl)
639     return 1;
640   if (!a_plvl && b_plvl)
641     return -1;
642
643   if (a_major > b_major)
644     return 1;
645   if (a_major < b_major)
646     return -1;
647
648   if (a_minor > b_minor)
649     return 1;
650   if (a_minor < b_minor)
651     return -1;
652
653   if (a_micro > b_micro)
654     return 1;
655   if (a_micro < b_micro)
656     return -1;
657
658   if (opt_reverse_ver && !opt_reverse)
659     {
660       /* We may only compare up to the next dot and the swicth back to
661        * regular order.  */
662       for (; *a_plvl && *b_plvl; a_plvl++, b_plvl++)
663         {
664           if (*a_plvl == '.' && *b_plvl == '.')
665             return 0 - strcmp (a_plvl, b_plvl);
666           else if (*a_plvl == '.')
667             return 1;  /* B is larger but we need to reverse. */
668           else if (*b_plvl == '.')
669             return -1; /* A is larger but we need to reverse. */
670           else if (*a_plvl != *b_plvl)
671             break;
672         }
673       if (*a_plvl == *b_plvl)
674         return 0;
675       else
676         return (*(signed char *)b_plvl - *(signed char *)a_plvl);
677     }
678   else
679     return strcmp (a_plvl, b_plvl);
680 }
681
682
683 /* If string looks like a file name with a version nuymber, return a
684  * pointer to the version number part; else return NULL.  */
685 static const char *
686 find_version_string (const char *s)
687 {
688   do
689     {
690       s = strchr (s, '-');
691       if (!s)
692         return NULL; /* Version string must be prefixed with a dash.  */
693       s++;
694     }
695   while (!digitp (*s));
696
697   return s;
698 }
699
700
701 /* Sort function for the directory listing.  */
702 static int
703 sort_finfo (const void *arg_a, const void *arg_b)
704 {
705   const finfo_t *a = arg_a;
706   const finfo_t *b = arg_b;
707   const char *astr, *bstr;
708   const char *aver, *bver;
709
710   if (opt_reverse)
711     {
712       astr = (*b)->name;
713       bstr = (*a)->name;
714     }
715   else
716     {
717       astr = (*a)->name;
718       bstr = (*b)->name;
719     }
720
721   aver = find_version_string (astr);
722   bver = aver? find_version_string (bstr) : NULL;
723
724   if (aver && bver
725       && (aver - astr) == (bver - bstr)
726       && !memcmp (astr, bstr, (aver - astr)))
727     {
728       if (opt_reverse_ver)
729         return 0 - compare_version_strings (aver, bver);
730       else
731         return compare_version_strings (aver, bver);
732     }
733
734   return strcmp(astr, bstr);
735 }
736
737
738 /* Note: This function assumes that the CWD is the listed directory.  */
739 static void
740 print_header (const char *title)
741 {
742   const char *esc_title;
743
744   if (!opt_html)
745     return;
746
747   esc_title = html_escape (title);
748
749   if (opt_gpgweb)
750     {
751       FILE *readme;
752       char line[256];
753       char *p;
754       int c;
755
756       fputs ("<!--?xml version=\"1.0\" encoding=\"utf-8\"?-->\n"
757              "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\""
758              " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
759              "<html xmlns=\"http://www.w3.org/1999/xhtml\""
760              " xml:lang=\"en\" lang=\"en\">\n", stdout);
761       printf("<head>\n"
762              "<title>ftp.gnupg.org:%s</title>\n",
763              esc_title);
764       fputs ("<meta http-equiv=\"Content-Type\""
765              " content=\"text/html; charset=UTF-8\"/>\n", stdout);
766       printf("<meta name=\"date\" content=\"%s\"/>\n", format_time_now (0));
767       fputs ("<meta name=\"generator\" content=\""PGMNAME" v"VERSION"\"/>\n"
768              "<meta name=\"viewport\""
769              " content=\"width=device-width, initial-scale=1\"/>\n"
770              "<link rel=\"stylesheet\" href=\"/share/site.css\""
771              " type=\"text/css\"/>\n"
772              "</head>\n", stdout);
773
774       fputs ("<body>\n"
775              "<div id=\"wrapper\">\n"
776              "<div id=\"header\"><a href=\"https://gnupg.org/index.html\""
777              " class=\"logo\">"
778              "<img src=\"/share/logo-gnupg-light-purple-bg.png\"></a>&nbsp;"
779              "</div>\n", stdout);
780
781       printf("<main>\n"
782              "<div id=\"content\">\n"
783              "<h2>ftp.gnupg.org:%s</h2>\n"
784              "<div class=\"outline-text-2\" id=\"text-1\">\n",
785              esc_title);
786
787       readme = fopen ("README", "r");
788       if (opt_readme && (readme = fopen ("README", "r")))
789         {
790           fputs ("<pre class=\"ftp-readme\">\n", stdout);
791           while (fgets (line, sizeof line, readme))
792             {
793               int no_lf = 0;
794               /* Eat up the rest of an incomplete line.  */
795               if (!*line)
796                 no_lf = 1;
797               else if (line[strlen (line)-1] != '\n')
798                 {
799                   no_lf = 1;
800                   while ((c = getc (readme)) != EOF && c != '\n')
801                     ;
802                 }
803
804               /* Replace empty lines with a leading doc by an empty
805                * line.  These lines are used on FTP servers to avoid
806                * problems with broken FTP cleints.  */
807               if (*line == '.')
808                 {
809                   for (p=line+1; (*p == ' ' || *p == '\t' || *p == '\n'); p++)
810                     ;
811                   if (!*p)
812                     {
813                       putchar ('\n');
814                       *line = 0;
815                     }
816                 }
817
818               if (*line)
819                 fputs (html_escape_detect_link (line), stdout);
820               if (no_lf)
821                 putchar ('\n');
822             }
823           fputs ("</pre>\n", stdout);
824           fclose (readme);
825         }
826       fputs ("</div>\n", stdout);
827
828    }
829   else
830     {
831       printf ("<html>\n"
832               "<head>\n"
833               "<title>Index of %s</title>\n"
834               "</head>\n"
835               "<body bgcolor=\"#ffffff\">\n"
836               "<h2>Index of %s</h2>\n"
837               "<table class=\"ftp\">\n",
838               esc_title, esc_title);
839     }
840 }
841
842
843 static void
844 print_footer (void)
845 {
846   if (!opt_html)
847     return;
848
849   if (opt_gpgweb)
850     {
851       fputs ("</div><!-- end content -->\n"
852              "</main>\n"
853              "<div id=\"footer\">\n", stdout);
854       fputs ("<div id=\"nav_bottom\">\n"
855              "<ul>\n"
856              "<li><a href=\"/privacy-policy.html\">Privacy&nbsp;Policy</a>"
857              "</li>\n"
858              "<li><a href=\"/imprint.html\">Imprint</a>"
859              "</li>\n"
860              "<li><a href=\"/blog/index.html\">Blog</a>"
861              "</li>\n"
862              "<li><a href=\"/index.html\">Web</a>"
863              "</li>\n"
864              "</ul>\n"
865              "</div>\n", stdout);
866
867       fputs ("<div class=\"footerbox\">\n"
868              "<a><img src=\"/share/traueranzeige-g10_v2015.png\""
869              " alt=\"Traueranzeige: Wir nehmen Abschied von einem"
870              " sicher geglaubten Freund, dem | Fernmeldegeheimniss"
871              " | (Artikel 10 Grundgesetz) | * 23. Mai 1949,"
872              " + 18. Dezember 2015\" title=\"Article 10 of the"
873              " German constitution (communication privacy) is"
874              " not anymore with us.\" height=\"73px\" width=\"200px\"></a>\n"
875              "<p></p>\n"
876              "</div>\n", stdout);
877
878       fputs ("<div id=\"cpyright\">\n"
879              "<a rel=\"license\""
880              " href=\"https://creativecommons.org/licenses/by-sa/4.0/\">"
881              "<img alt=\"CC BY-SA 4.0\" style=\"border: 0\""
882              " src=\"/share/cc-by-sa_80x15.png\"></a>&nbsp;"
883              "This web page is Copyright 2017 GnuPG e.V. and"
884              " licensed under a <a rel=\"license\""
885              " href=\"https://creativecommons.org/licenses/by-sa/4.0/\">"
886              "Creative Commons Attribution-ShareAlike 4.0 International"
887              " License</a>.  See <a href=\"https://gnupg.org/copying.html\">"
888              "copying</a> for details.\n", stdout);
889       printf("Page last updated on %s.\n", format_time_now (1));
890       fputs ("</div>\n"
891              "</div>\n"
892              "</div><!-- end wrapper -->\n"
893              "</body>\n"
894              "</html>\n", stdout);
895     }
896   else
897     {
898       printf ("</table>\n"
899               "</body>\n"
900               "</html>\n");
901     }
902 }
903
904
905 /* Print COUNT directories from the array SORTED. */
906 static void
907 print_dirs (finfo_t *sorted, int count, int at_root)
908 {
909   int idx;
910   finfo_t fi;
911   int any = 0;
912
913   for (idx=0; idx < count; idx++)
914     {
915       fi = sorted[idx];
916       if (!fi->is_dir)
917         continue;
918
919       if (!any && opt_html)
920         {
921           any = 1;
922
923           if (opt_gpgweb)
924             {
925               fputs ("<h3>Directories</h3>\n"
926                      "<div class=\"outline-text-3\">\n"
927                      "<table class=\"ftp\">\n", stdout);
928
929               if (!at_root)
930                 fputs ("<tr><td><img src=\"/share/up.png\""
931                        " width=\"22\" height=\"22\"/></td>"
932                        "<td><a href=\"../\">"
933                        "Parent Directory</a></td></tr>\n", stdout);
934             }
935           else
936             {
937               fputs ("<tr><td>&nbsp</td>"
938                      "<td colspan=3><h3>Directories</h3></td></tr>\n",
939                      stdout);
940               if (!at_root)
941                 fputs ("<tr><td><a href=\"../\">"
942                        "Parent Directory</a></td></tr>\n", stdout);
943             }
944         }
945
946       if (opt_gpgweb)
947         printf ("<tr><td><img src=\"/share/folder.png\""
948                 " width=\"22\" height=\"22\"/></td>"
949                 "<td><a href=\"%s\">%s</a></td></tr>\n",
950                 html_escape_href (fi->name), html_escape (fi->name));
951       else if (opt_html)
952         printf ("<tr><td width=\"40%%\"><a href=\"%s\">%s</a></td></tr>\n",
953                 html_escape_href (fi->name), html_escape (fi->name));
954       else
955         printf ("D %s\n", fi->name);
956     }
957
958   if (any && opt_gpgweb)
959     {
960       fputs ("</table>\n"
961              "</div>\n\n", stdout);
962     }
963   else if (opt_gpgweb && !at_root)
964     {
965       /* !any - need to print an UP link */
966       fputs ("<div class=\"outline-text-3\">\n"
967              "<table class=\"ftp\">\n"
968              "<tr><td><img src=\"/share/up.png\""
969              " width=\"22\" height=\"22\"/></td>"
970              "<td><a href=\"../\">"
971              "Parent Directory</a></td></tr>\n"
972              "</table>\n"
973              "</div>\n", stdout);
974
975
976     }
977 }
978
979
980 /* Print COUNT files from the array SORTED. */
981 static void
982 print_files (finfo_t *sorted, int count)
983 {
984   int idx;
985   finfo_t fi;
986   int any = 0;
987
988   for (idx=0; idx < count; idx++)
989     {
990       fi = sorted[idx];
991       if (!fi->is_reg)
992         continue;
993
994       if (!any && opt_html)
995         {
996           any = 1;
997           if (opt_gpgweb)
998             {
999               fputs ("<h3>Files</h3>\n"
1000                      "<div class=\"outline-text-3\">\n"
1001                      "<table class=\"ftp\">\n", stdout);
1002             }
1003           else
1004             fputs ("<tr><td colspan=3><h3>Files</h3></td></tr>\n",
1005                    stdout);
1006
1007         }
1008
1009       if (opt_gpgweb)
1010         printf ("<tr><td><img src=\"/share/%s.png\""
1011                 " width=\"22\" height=\"22\"/></td>"
1012                 "<td><a href=\"%s\">%s</a></td>"
1013                 "<td align=\"right\">%s</td><td align=\"right\">%s</td></tr>\n",
1014                 strstr (fi->name, ".sig")? "document":
1015                 strstr (fi->name, ".tar")? "tar" : "document",
1016                 html_escape_href (fi->name), html_escape (fi->name),
1017                 format_time (fi->mtime), format_size (fi->size));
1018       else if (opt_html)
1019         printf ("<tr><td width=\"50%%\"><a href=\"%s\">%s</a></td>"
1020                 "<td align=\"right\">%s</td><td align=\"right\">%s</td></tr>\n",
1021                 html_escape_href (fi->name), html_escape (fi->name),
1022                 format_time (fi->mtime), format_size (fi->size));
1023       else
1024         printf ("F %s\n", fi->name);
1025     }
1026
1027   if (any && opt_gpgweb)
1028     {
1029       fputs ("</table>\n"
1030              "</div>\n\n", stdout);
1031     }
1032 }
1033
1034
1035 /* Scan DIRECTORY and print an index.
1036  * FIXME: This does a chdir and does not preserve the old PWD.
1037  *        The fix is to build the full filename beofre stat'ing.
1038  */
1039 static void
1040 scan_directory (const char *directory, const char *title)
1041 {
1042   DIR *dir;
1043   struct dirent *dentry;
1044   finfo_t fi;
1045   finfo_t finfo = NULL;
1046   finfo_t *sorted;
1047   int count = 0;
1048   int idx;
1049   size_t len;
1050   strlist_t sl;
1051   int at_root = 0;
1052
1053   if (opt_gpgweb)
1054     {
1055       if (!strcmp (title, "/"))
1056         at_root = 1;
1057     }
1058   else if (!strcmp (directory, "/"))
1059     at_root = 1;
1060
1061   dir = opendir (directory);
1062   if (!dir)
1063     {
1064       err ("can't open directory '%s': %s\n", directory, strerror (errno));
1065       return;
1066     }
1067
1068   while (errno=0,(dentry = readdir (dir)))
1069     {
1070       if (*dentry->d_name == '.')
1071         continue;  /* Skip self, parent, and hidden directories.  */
1072       len = strlen (dentry->d_name);
1073       if (!len)
1074         continue;  /* Empty filenames should actually not exist.  */
1075       if (dentry->d_name[len-1] == '~')
1076         continue;  /* Skip backup files.  */
1077       for (sl = opt_exclude; sl; sl = sl->next)
1078         if (!strcmp (sl->d, dentry->d_name))
1079           break;
1080       if (sl)
1081         continue; /* Skip excluded names.  */
1082       fi = xcalloc (1, sizeof *fi + strlen (dentry->d_name));
1083       strcpy (fi->name, dentry->d_name);
1084       fi->next = finfo;
1085       finfo = fi;
1086       count++;
1087     }
1088   if (errno)
1089     die ("error reading directory '%s': %s\n", directory, strerror (errno));
1090   closedir (dir);
1091
1092   sorted = xcalloc (count, sizeof *sorted);
1093   for (fi=finfo, idx=0; fi; fi = fi->next)
1094     sorted[idx++] = fi;
1095
1096   inf ("directory '%s' has %d files\n", directory, count);
1097   qsort (sorted, count, sizeof *sorted, sort_finfo);
1098
1099   if (chdir (directory))
1100     die ("cannot chdir to '%s': %s\n", directory, strerror (errno));
1101
1102   for (idx=0; idx < count; idx++)
1103     {
1104       struct stat sb;
1105
1106       fi = sorted[idx];
1107       if (stat (fi->name, &sb))
1108         {
1109           err ("cannot stat '%s': %s\n", fi->name, strerror (errno));
1110           continue;
1111         }
1112
1113       fi->is_dir = !!S_ISDIR(sb.st_mode);
1114       fi->is_reg = !!S_ISREG(sb.st_mode);
1115       fi->size = fi->is_reg? sb.st_size : 0;
1116       fi->mtime = sb.st_mtime;
1117     }
1118
1119   print_header (title);
1120   if (opt_files_first)
1121     {
1122       print_files (sorted, count);
1123       print_dirs (sorted, count, at_root);
1124     }
1125   else
1126     {
1127       print_dirs (sorted, count, at_root);
1128       print_files (sorted, count);
1129     }
1130   print_footer ();
1131
1132   /* We create the index file in the current directory.  */
1133   if (opt_index)
1134     {
1135       FILE *indexfp = fopen (opt_index, "w");
1136       if (!indexfp)
1137         die ("error creating '%s' for '%s': %s\n",
1138              opt_index, directory, strerror (errno));
1139
1140       for (idx=0; idx < count; idx++)
1141         {
1142           fi = sorted[idx];
1143           fprintf (indexfp, "%s:%c:%llu:%lu:\n",
1144                    percent_escape (fi->name),
1145                    fi->is_dir? 'd':
1146                    fi->is_reg? 'r': '?',
1147                    fi->size,
1148                    (unsigned long)fi->mtime);
1149         }
1150       if (ferror (indexfp))
1151         die ("error writing '%s' for '%s': %s\n",
1152              opt_index, directory, strerror (errno));
1153       /* Fixme: Check for close errors.  */
1154       fclose (indexfp);
1155     }
1156
1157   free (sorted);
1158   while ((fi = finfo))
1159     {
1160       fi = finfo->next;
1161       free (finfo);
1162       finfo = fi;
1163     }
1164 }
1165
1166
1167 int
1168 main (int argc, char **argv)
1169 {
1170   int last_argc = -1;
1171   strlist_t sl;
1172
1173   if (argc < 1)
1174     die ("Hey, read up on how to use exec(2)\n");
1175   argv++; argc--;
1176
1177   while (argc && last_argc != argc )
1178     {
1179       last_argc = argc;
1180       if (!strcmp (*argv, "--"))
1181         {
1182           argc--; argv++;
1183           break;
1184         }
1185       else if (!strcmp (*argv, "--version"))
1186         {
1187           fputs (PGMNAME " " VERSION "\n"
1188                  "Copyright (C) 2017 g10 Code GmbH\n"
1189                  "License GPLv3+: GNU GPL version 3 or later"
1190                  " <https://gnu.org/licenses/gpl.html>\n"
1191                  "This is free software: you are free to change"
1192                  " and redistribute it.\n"
1193                  "There is NO WARRANTY, to the extent permitted by law.\n",
1194                  stdout);
1195           exit (0);
1196         }
1197       else if (!strcmp (*argv, "--help"))
1198         {
1199           fputs ("usage: " PGMNAME " [options] directory [title]\n"
1200                  "Print an index for an FTP directory.\n\n"
1201                  "Options:\n"
1202                  "  --version       print program version\n"
1203                  "  --verbose       verbose diagnostics\n"
1204                  "  --debug         flyswatter\n"
1205                  "  --reverse       reverse sort order\n"
1206                  "  --reverse-ver   reverse only the version number order\n"
1207                  "  --files-first   print files before directories\n"
1208                  "  --html          output HTML\n"
1209                  "  --gpgweb        output HTML as used at gnupg.org\n"
1210                  "  --readme        include README file\n"
1211                  "  --index FILE    create index FILE\n"
1212                  "  --exclude NAME  ignore file NAME\n"
1213                  , stdout);
1214           exit (0);
1215         }
1216       else if (!strcmp (*argv, "--verbose"))
1217         {
1218           opt_verbose++;
1219           argc--; argv++;
1220         }
1221       else if (!strcmp (*argv, "--debug"))
1222         {
1223           opt_debug++;
1224           argc--; argv++;
1225         }
1226       else if (!strcmp (*argv, "--reverse"))
1227         {
1228           opt_reverse = 1;
1229           argc--; argv++;
1230         }
1231       else if (!strcmp (*argv, "--reverse-ver"))
1232         {
1233           opt_reverse_ver = 1;
1234           argc--; argv++;
1235         }
1236       else if (!strcmp (*argv, "--files-first"))
1237         {
1238           opt_files_first = 1;
1239           argc--; argv++;
1240         }
1241       else if (!strcmp (*argv, "--readme"))
1242         {
1243           opt_readme = 1;
1244           argc--; argv++;
1245         }
1246       else if (!strcmp (*argv, "--html"))
1247         {
1248           opt_html = 1;
1249           argc--; argv++;
1250         }
1251       else if (!strcmp (*argv, "--index"))
1252         {
1253           argc--; argv++;
1254           if (!argc || !**argv)
1255             die ("argument missing for option '%s'\n", argv[-1]);
1256           opt_index = *argv;
1257           argc--; argv++;
1258         }
1259       else if (!strcmp (*argv, "--gpgweb"))
1260         {
1261           opt_gpgweb = opt_html = 1;
1262           argc--; argv++;
1263         }
1264       else if (!strcmp (*argv, "--exclude"))
1265         {
1266           argc--; argv++;
1267           if (!argc || !**argv)
1268             die ("argument missing for option '%s'\n", argv[-1]);
1269           sl = xmalloc (sizeof *sl + strlen (*argv));
1270           strcpy (sl->d, *argv);
1271           sl->next = opt_exclude;
1272           opt_exclude = sl;
1273           argc--; argv++;
1274         }
1275       else if (!strncmp (*argv, "--", 2))
1276         die ("unknown option '%s' (use --help)\n", *argv);
1277     }
1278
1279   if (argc < 1 || argc > 2)
1280     die ("usage: " PGMNAME " [options] directory [title]\n");
1281
1282
1283   scan_directory (argv[0], argv[1]? argv[1]:argv[0]);
1284
1285
1286   return 0;
1287 }
1288
1289 /*
1290 Local Variables:
1291 compile-command: "cc -Wall -g -o ftp-indexer ftp-indexer.c"
1292 End:
1293 */