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