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