diff --git a/tools/ftp-indexer.c b/tools/ftp-indexer.c new file mode 100644 index 0000000..5457f3d --- /dev/null +++ b/tools/ftp-indexer.c @@ -0,0 +1,1169 @@ +/* ftp-indexer.c - Create an HTML index file for an FTP directory + * Copyright (C) 2017 g10 Code GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* How to use: + * + **/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define PGMNAME "ftp-indexer" + + +#define DIM(v) (sizeof(v)/sizeof((v)[0])) +#define DIMof(type,member) DIM(((type *)0)->member) +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) +#define ATTR_PRINTF(a,b) __attribute__ ((format (printf,a,b))) +#define ATTR_NR_PRINTF(a,b) __attribute__ ((noreturn,format (printf,a,b))) +#else +#define ATTR_PRINTF(a,b) +#define ATTR_NR_PRINTF(a,b) +#endif + +#define digitp(a) ((a) >= '0' && (a) <= '9') +#define VALID_URI_CHARS "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ + "01234567890@" \ + "!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~" + + +/* A simple object to keep strings in a list. */ +struct strlist_s +{ + struct strlist_s *next; + char d[1]; +}; +typedef struct strlist_s *strlist_t; + + +/* An object to collect information about files. */ +struct finfo_s +{ + struct finfo_s *next; + unsigned int is_dir:1; + unsigned int is_reg:1; + time_t mtime; + unsigned long long size; + char name[1]; +}; +typedef struct finfo_s *finfo_t; + + +static int opt_verbose; +static int opt_debug; +static int opt_reverse; +static int opt_reverse_ver; +static int opt_files_first; +static int opt_html; +static int opt_index; +static int opt_gpgweb; +static int opt_readme; +static strlist_t opt_exclude; + +static void die (const char *format, ...) ATTR_NR_PRINTF(1,2); +static void err (const char *format, ...) ATTR_PRINTF(1,2); +static void inf (const char *format, ...) ATTR_PRINTF(1,2); + + + +static void +die (const char *fmt, ...) +{ + va_list arg_ptr; + + va_start (arg_ptr, fmt); + fputs (PGMNAME": ", stderr); + vfprintf (stderr, fmt, arg_ptr); + va_end (arg_ptr); + exit (1); +} + + +static void +err (const char *fmt, ...) +{ + va_list arg_ptr; + + va_start (arg_ptr, fmt); + fputs (PGMNAME": ", stderr); + vfprintf (stderr, fmt, arg_ptr); + va_end (arg_ptr); +} + + +static void +inf (const char *fmt, ...) +{ + va_list arg_ptr; + + if (!opt_verbose) + return; + + va_start (arg_ptr, fmt); + fputs (PGMNAME": ", stderr); + vfprintf (stderr, fmt, arg_ptr); + va_end (arg_ptr); +} + + +static void * +xmalloc (size_t n) +{ + void *p = malloc (n); + if (!p) + die ("out of core\n"); + return p; +} + + +static void * +xcalloc (size_t n, size_t k) +{ + void *p = calloc (n, k); + if (!p) + die ("out of core\n"); + return p; +} + + +static char * +xstrdup (const char *string) +{ + char *buf = xmalloc (strlen (string)); + strcpy (buf, string); + return buf; +} + + +static inline char * +my_stpcpy (char *a, const char *b) +{ + while (*b) + *a++ = *b++; + *a = 0; + + return (char*)a; +} + + +/* If SPECIAL is NULL this function escapes in forms mode. */ +static size_t +escape_data (char *buffer, const void *data, size_t datalen, + const char *special) +{ + int forms = !special; + const unsigned char *s; + size_t n = 0; + + if (forms) + special = "%;?&="; + + for (s = data; datalen; s++, datalen--) + { + if (forms && *s == ' ') + { + if (buffer) + *buffer++ = '+'; + n++; + } + else if (forms && *s == '\n') + { + if (buffer) + memcpy (buffer, "%0D%0A", 6); + n += 6; + } + else if (forms && *s == '\r' && datalen > 1 && s[1] == '\n') + { + if (buffer) + memcpy (buffer, "%0D%0A", 6); + n += 6; + s++; + datalen--; + } + else if (strchr (VALID_URI_CHARS, *s) && !strchr (special, *s)) + { + if (buffer) + *(unsigned char*)buffer++ = *s; + n++; + } + else + { + if (buffer) + { + snprintf (buffer, 4, "%%%02X", *s); + buffer += 3; + } + n += 3; + } + } + return n; +} + + +static int +insert_escapes (char *buffer, const char *string, + const char *special) +{ + return escape_data (buffer, string, strlen (string), special); +} + + +/* Allocate a new string from STRING using standard HTTP escaping as + * well as escaping of characters given in SPECIALS. A common pattern + * for SPECIALS is "%;?&=". However it depends on the needs, for + * example "+" and "/: often needs to be escaped too. Returns NULL on + * failure and sets ERRNO. If SPECIAL is NULL a dedicated forms + * encoding mode is used. */ +static char * +http_escape_string (const char *string, const char *specials) +{ + int n; + char *buf; + + n = insert_escapes (NULL, string, specials); + buf = xmalloc (n+1); + insert_escapes (buf, string, specials); + buf[n] = 0; + return buf; +} + + +/* Same as http_escape_string but with an explict length. */ +static char * +http_escape_buffer (const char *string, size_t length, const char *specials) +{ + int n; + char *buf; + + n = escape_data (NULL, string, length, specials); + buf = xmalloc (n+1); + escape_data (buf, string, length, specials); + buf[n] = 0; + return buf; +} + + +/* Percent-escape the string STR by replacing colons with '%3a'. */ +static char * +do_percent_escape (const char *str) +{ + int i, j; + char *ptr; + + for (i=j=0; str[i]; i++) + if (str[i] == ':' || str[i] == '%' || str[i] == '\n') + j++; + ptr = xmalloc (i + 2 * j + 1); + i = 0; + while (*str) + { + if (*str == ':') + { + ptr[i++] = '%'; + ptr[i++] = '3'; + ptr[i++] = 'a'; + } + else if (*str == '%') + { + ptr[i++] = '%'; + ptr[i++] = '2'; + ptr[i++] = '5'; + } + else if (*str == '\n') + { + /* The newline is problematic in a line-based format. */ + ptr[i++] = '%'; + ptr[i++] = '0'; + ptr[i++] = 'a'; + } + else + ptr[i++] = *str; + str++; + } + ptr[i] = '\0'; + + return ptr; +} + + +/* Simple percent escape for colon based listings. Returns a + * statically allocated buffer. */ +static char * +percent_escape (const char *str) +{ + static char *buffer; + + free (buffer); + buffer = do_percent_escape (str); + return buffer; +} + + +/* Escape STRING at a max length of N for use in HTML. Returns a + * statically allocated buffer. */ +static const char * +html_escape_n (const char *string, size_t length) +{ + static char *buffer; + char *p; + + /* The escaped string may be up to 6 times of STRING due to the + expansion of '\"' to """. */ + free (buffer); + p = buffer = xmalloc (6 * length + 1); + for (; *string && length; string++, length--) + { + switch (*string) + { + case '\"': p = my_stpcpy (p, """); break; + case '&': p = my_stpcpy (p, "&"); break; + case '<': p = my_stpcpy (p, "<"); break; + case '>': p = my_stpcpy (p, ">"); break; + default: *p++ = *string; break; + } + } + *p = 0; + return buffer; +} + + +/* Escape STRING for use in HTML. Returns a statically allocated + * buffer. noet that this buffer is shared with the buffer returned + * by html_escape_n. */ +static const char * +html_escape (const char *string) +{ + return html_escape_n (string, strlen (string)); +} + + +/* Escape STRING but insert for one https link. */ +static const char * +html_escape_detect_link (const char *string) +{ + const char *start, *s; + char *part1, *url, *part2, *part3; + size_t urllen; + char *buffer, *p; + + start = strstr (string, "https://"); + if (!start || !start[8] || start[8] == ' ' || start[8] == '\t') + return html_escape (string); + if (!(start == string || start[-1] == ' ' || start[-1] == '\t' + || start[-1] == '<')) + return html_escape (string); + + urllen = 0; + for (s = start; *s && *s != ' ' && *s != '\t' && *s != '>'; s++) + urllen++; + + part1 = xstrdup (html_escape_n (string, start-string)); + url = http_escape_buffer (start, urllen, "\""); + part2 = xstrdup (html_escape_n (start, urllen)); + part3 = xstrdup (html_escape (start + urllen)); + + buffer = xmalloc (strlen (part1) + strlen (url) + + strlen (part2) + strlen (part3) + 100); + p = my_stpcpy (buffer, part1); + p = my_stpcpy (p, ""); + p = my_stpcpy (p, part2); + p = my_stpcpy (p, ""); + my_stpcpy (p, part3); + + free (part1); + free (url); + free (part2); + free (part3); + return buffer; +} + + + +/* Escape STRING for use as a HREF attribute. Returns a statically + * allocated buffer. */ +static const char * +html_escape_href (const char *string) +{ + static char *buffer; + + free (buffer); + buffer = http_escape_string (string, "\""); + return buffer; +} + + +/* Format T and return a statically allocated buffer. */ +static const char * +format_time (time_t t) +{ + static char buffer[80]; + struct tm *tp; + + tp = gmtime (&t); + if (!tp) + *buffer = 0; + else if (opt_html) + snprintf (buffer, sizeof buffer, + "" + "%04d-%02d-%02d", + 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday, + tp->tm_hour, tp->tm_min, tp->tm_sec, + 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday); + else + snprintf (buffer, sizeof buffer, "%04d%02d%02dT%02d%02d%02dZ", + 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday, + tp->tm_hour, tp->tm_min, tp->tm_sec); + return buffer; +} + + +/* Format SIZE and return a statically allocated buffer. */ +static const char * +format_size (unsigned long long size) +{ + static char buffer[80]; + const char *suffix; + unsigned long long val = size; + + if (size < 1024) + { + val = size; + suffix = ""; + } + else if (size < 1024 * 1024) + { + val = size / 1024; + suffix = "k"; + } + else if (size < 1024 * 1024 * 1024) + { + val = size / (1024 * 1024); + suffix = "M"; + } + else + { + val = size / (1024 * 1024 * 1024); + suffix = "G"; + } + + if (opt_html) + snprintf (buffer, sizeof buffer, + "%llu%s", + size, size == 1? "":"s", + val, suffix); + else + snprintf (buffer, sizeof buffer, "%llu%s", val, suffix); + + return buffer; +} + + +/* This function parses the first portion of the version number S and + * stores it at NUMBER. On success, this function returns a pointer + * into S starting with the first character, which is not part of the + * initial number portion; on failure, NULL is returned. */ +static const char* +parse_version_number (const char *s, int *number) +{ + int val = 0; + + if (*s == '0' && digitp (s[1])) + return NULL; /* Leading zeros are not allowed. */ + for (; digitp (*s); s++) + { + val *= 10; + val += *s - '0'; + } + *number = val; + return val < 0 ? NULL : s; +} + + +/* This function breaks up the complete string-representation of the + * version number S, which is of the following struture: ... The major, + * minor and micro number components will be stored in *MAJOR, *MINOR + * and *MICRO. + * + * On success, the last component, the patch level, will be returned; + * in failure, NULL will be returned. */ +static const char * +parse_version_string (const char *s, int *major, int *minor, int *micro) +{ + s = parse_version_number (s, major); + if (!s || *s != '.') + return NULL; + s++; + s = parse_version_number (s, minor); + if (!s || *s != '.') + return NULL; + s++; + s = parse_version_number (s, micro); + if (!s) + return NULL; + return s; /* patchlevel */ +} + + +/* Compare function for version strings. */ +static int +compare_version_strings (const char *a, const char *b) +{ + int a_major, a_minor, a_micro; + int b_major, b_minor, b_micro; + const char *a_plvl, *b_plvl; + + a_plvl = parse_version_string (a, &a_major, &a_minor, &a_micro); + if (!a_plvl) + a_major = a_minor = a_micro = 0; + + b_plvl = parse_version_string (b, &b_major, &b_minor, &b_micro); + if (!b_plvl) + b_major = b_minor = b_micro = 0; + + if (!a_plvl && !b_plvl) + return -1; /* Put invalid strings at the end. */ + if (a_plvl && !b_plvl) + return 1; + if (!a_plvl && b_plvl) + return -1; + + if (a_major > b_major) + return 1; + if (a_major < b_major) + return -1; + + if (a_minor > b_minor) + return 1; + if (a_minor < b_minor) + return -1; + + if (a_micro > b_micro) + return 1; + if (a_micro < b_micro) + return -1; + + if (opt_reverse_ver && !opt_reverse) + { + /* We may only compare up to the next dot and the swicth back to + * regular order. */ + for (; *a_plvl && *b_plvl; a_plvl++, b_plvl++) + { + if (*a_plvl == '.' && *b_plvl == '.') + return 0 - strcmp (a_plvl, b_plvl); + else if (*a_plvl == '.') + return 1; /* B is larger but we need to reverse. */ + else if (*b_plvl == '.') + return -1; /* A is larger but we need to reverse. */ + else if (*a_plvl != *b_plvl) + break; + } + if (*a_plvl == *b_plvl) + return 0; + else + return (*(signed char *)b_plvl - *(signed char *)a_plvl); + } + else + return strcmp (a_plvl, b_plvl); +} + + +/* If string looks like a file name with a version nuymber, return a + * pointer to the version number part; else return NULL. */ +static const char * +find_version_string (const char *s) +{ + do + { + s = strchr (s, '-'); + if (!s) + return NULL; /* Version string must be prefixed with a dash. */ + s++; + } + while (!digitp (*s)); + + return s; +} + + +/* Sort function for the directory listing. */ +static int +sort_finfo (const void *arg_a, const void *arg_b) +{ + const finfo_t *a = arg_a; + const finfo_t *b = arg_b; + const char *astr, *bstr; + const char *aver, *bver; + + if (opt_reverse) + { + astr = (*b)->name; + bstr = (*a)->name; + } + else + { + astr = (*a)->name; + bstr = (*b)->name; + } + + aver = find_version_string (astr); + bver = aver? find_version_string (bstr) : NULL; + + if (aver && bver + && (aver - astr) == (bver - bstr) + && !memcmp (astr, bstr, (aver - astr))) + { + if (opt_reverse_ver) + return 0 - compare_version_strings (aver, bver); + else + return compare_version_strings (aver, bver); + } + + return strcmp(astr, bstr); +} + + +/* Note: This function assumes that the CWD is the listed directory. */ +static void +print_header (const char *title) +{ + const char *esc_title; + + if (!opt_html) + return; + + esc_title = html_escape (title); + + if (opt_gpgweb) + { + FILE *readme; + char line[256]; + char *p; + int c; + + fputs ("\n" + "\n" + "\n", stdout); + printf("\n" + "ftp.gnupg.org - %s\n", + esc_title); + fputs ("\n" + "\n" + "\n" + "\n", stdout); + + fputs ("\n" + "
\n" + "
" + " " + "
\n", stdout); + + printf("
\n" + "
\n" + "

%s

\n" + "
\n", + esc_title); + + readme = fopen ("README", "r"); + if (opt_readme && (readme = fopen ("README", "r"))) + { + fputs ("
\n", stdout);
+          while (fgets (line, sizeof line, readme))
+            {
+              int no_lf = 0;
+              /* Eat up the rest of an incomplete line.  */
+              if (!*line)
+                no_lf = 1;
+              else if (line[strlen (line)-1] != '\n')
+                {
+                  no_lf = 1;
+                  while ((c = getc (readme)) != EOF && c != '\n')
+                    ;
+                }
+
+              /* Replace empty lines with a leading doc by an empty
+               * line.  These lines are used on FTP servers to avoid
+               * problems with broken FTP cleints.  */
+              if (*line == '.')
+                {
+                  for (p=line+1; (*p == ' ' || *p == '\t' || *p == '\n'); p++)
+                    ;
+                  if (!*p)
+                    {
+                      putchar ('\n');
+                      *line = 0;
+                    }
+                }
+
+              if (*line)
+                fputs (html_escape_detect_link (line), stdout);
+              if (no_lf)
+                putchar ('\n');
+            }
+          fputs ("
\n", stdout); + fclose (readme); + } + fputs ("
\n", stdout); + + } + else + { + printf ("\n" + "\n" + "Index of %s\n" + "\n" + "\n" + "

Index of %s

\n" + "\n", + esc_title, esc_title); + } +} + + +static void +print_footer (void) +{ + if (!opt_html) + return; + + if (opt_gpgweb) + { + fputs ("\n" + "\n" + "
\n", stdout); + fputs ("
\n" + "\n" + "
\n", stdout); + + fputs ("
\n" + "\"Traueranzeige:\n" + "

\n" + "
\n", stdout); + + fputs ("
\n" + "" + "\"CC " + "This web page is Copyright 2017 GnuPG e.V. and" + " licensed under a " + "Creative Commons Attribution-ShareAlike 4.0 International" + " License. See " + "copying for details.\n", stdout); + printf("Page last updated on 2017-xx-xx.\n"); + fputs ("
\n" + "
\n" + "\n" + "\n" + "\n", stdout); + } + else + { + printf ("
\n" + "\n" + "\n"); + } +} + + +/* Print COUNT directories from the array SORTED. */ +static void +print_dirs (finfo_t *sorted, int count, const char *directory) +{ + int idx; + finfo_t fi; + int any = 0; + + for (idx=0; idx < count; idx++) + { + fi = sorted[idx]; + if (!fi->is_dir) + continue; + + if (!any && opt_html) + { + any = 1; + + if (opt_gpgweb) + { + fputs ("

Directories

\n" + "
\n" + "\n", stdout); + + if (strcmp (directory, "/")) + fputs ("" + "\n", stdout); + } + else + { + fputs ("" + "\n", + stdout); + if (strcmp (directory, "/")) + fputs ("\n", stdout); + } + } + + if (opt_gpgweb) + printf ("" + "\n", + html_escape_href (fi->name), html_escape (fi->name)); + else if (opt_html) + printf ("\n", + html_escape_href (fi->name), html_escape (fi->name)); + else + printf ("D %s\n", fi->name); + } + + if (any && opt_gpgweb) + { + fputs ("
" + "Parent Directory
 

Directories

" + "Parent Directory
%s
%s
\n" + "
\n\n", stdout); + } +} + + +/* Print COUNT files from the array SORTED. */ +static void +print_files (finfo_t *sorted, int count) +{ + int idx; + finfo_t fi; + int any = 0; + + for (idx=0; idx < count; idx++) + { + fi = sorted[idx]; + if (!fi->is_reg) + continue; + + if (!any && opt_html) + { + any = 1; + if (opt_gpgweb) + { + fputs ("

Files

\n" + "
\n" + "\n", stdout); + } + else + fputs ("\n", + stdout); + + } + + if (opt_gpgweb) + printf ("" + "" + "\n", + strstr (fi->name, ".sig")? "document": + strstr (fi->name, ".tar")? "tar" : "document", + html_escape_href (fi->name), html_escape (fi->name), + format_time (fi->mtime), format_size (fi->size)); + else if (opt_html) + printf ("" + "\n", + html_escape_href (fi->name), html_escape (fi->name), + format_time (fi->mtime), format_size (fi->size)); + else + printf ("F %s\n", fi->name); + } + + if (any && opt_gpgweb) + { + fputs ("

Files

%s%s%s
%s%s%s
\n" + "
\n\n", stdout); + } +} + + +/* Scan DIRECTORY and print an index. + * FIXME: This does a chdir and does not preserve the old PWD. + * The fix is to build the full filename beofre stat'ing. + */ +static void +scan_directory (const char *directory, const char *title) +{ + DIR *dir; + struct dirent *dentry; + finfo_t fi; + finfo_t finfo = NULL; + finfo_t *sorted; + int count = 0; + int idx; + size_t len; + strlist_t sl; + + dir = opendir (directory); + if (!dir) + { + err ("can't open directory '%s': %s\n", directory, strerror (errno)); + return; + } + + while (errno=0,(dentry = readdir (dir))) + { + if (*dentry->d_name == '.') + continue; /* Skip self, parent, and hidden directories. */ + len = strlen (dentry->d_name); + if (!len) + continue; /* Empty filenames should actually not exist. */ + if (dentry->d_name[len-1] == '~') + continue; /* Skip backup files. */ + for (sl = opt_exclude; sl; sl = sl->next) + if (!strcmp (sl->d, dentry->d_name)) + break; + if (sl) + continue; /* Skip excluded names. */ + fi = xcalloc (1, sizeof *fi + strlen (dentry->d_name)); + strcpy (fi->name, dentry->d_name); + fi->next = finfo; + finfo = fi; + count++; + } + if (errno) + die ("error reading directory '%s': %s\n", directory, strerror (errno)); + closedir (dir); + + sorted = xcalloc (count, sizeof *sorted); + for (fi=finfo, idx=0; fi; fi = fi->next) + sorted[idx++] = fi; + + inf ("directory '%s' has %d files\n", directory, count); + qsort (sorted, count, sizeof *sorted, sort_finfo); + + if (chdir (directory)) + die ("cannot chdir to '%s': %s\n", directory, strerror (errno)); + + for (idx=0; idx < count; idx++) + { + struct stat sb; + + fi = sorted[idx]; + if (stat (fi->name, &sb)) + { + err ("cannot stat '%s': %s\n", fi->name, strerror (errno)); + continue; + } + + fi->is_dir = !!S_ISDIR(sb.st_mode); + fi->is_reg = !!S_ISREG(sb.st_mode); + fi->size = fi->is_reg? sb.st_size : 0; + fi->mtime = sb.st_mtime; + } + + print_header (title); + if (opt_files_first) + { + print_files (sorted, count); + print_dirs (sorted, count, directory); + } + else + { + print_dirs (sorted, count, directory); + print_files (sorted, count); + } + print_footer (); + + /* We create the index file in the current directory. */ + if (opt_index) + { + FILE *indexfp = fopen (".ftp-index", "w"); + if (!indexfp) + die ("error creating .ftp-index in '%s': %s\n", + directory, strerror (errno)); + + for (idx=0; idx < count; idx++) + { + fi = sorted[idx]; + fprintf (indexfp, "%s:%c:%llu:%lu:\n", + percent_escape (fi->name), + fi->is_dir? 'd': + fi->is_reg? 'r': '?', + fi->size, + (unsigned long)fi->mtime); + } + if (ferror (indexfp)) + die ("error writing .ftp-index in '%s': %s\n", + directory, strerror (errno)); + /* Fixme: Check for close errors. */ + fclose (indexfp); + } + + free (sorted); + while ((fi = finfo)) + { + fi = finfo->next; + free (finfo); + finfo = fi; + } +} + + +int +main (int argc, char **argv) +{ + int last_argc = -1; + strlist_t sl; + + if (argc < 1) + die ("Hey, read up on how to use exec(2)\n"); + argv++; argc--; + + while (argc && last_argc != argc ) + { + last_argc = argc; + if (!strcmp (*argv, "--")) + { + argc--; argv++; + break; + } + else if (!strcmp (*argv, "--help")) + { + fputs ("usage: " PGMNAME " [options] directory [title]\n" + "Print an index for an FTP directory.\n\n" + "Options:\n" + " --verbose verbose diagnostics\n" + " --debug flyswatter\n" + " --reverse reverse sort order\n" + " --reverse-ver reverse only the version number order\n" + " --files-first print files before directories\n" + " --html output HTML\n" + " --gpgweb output HTML as used at gnupg.org\n" + " --readme include README file\n" + " --index create an .ftp-index file\n" + " --exclude NAME ignore file NAME\n" + , stdout); + exit (0); + } + else if (!strcmp (*argv, "--verbose")) + { + opt_verbose++; + argc--; argv++; + } + else if (!strcmp (*argv, "--debug")) + { + opt_debug++; + argc--; argv++; + } + else if (!strcmp (*argv, "--reverse")) + { + opt_reverse = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--reverse-ver")) + { + opt_reverse_ver = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--files-first")) + { + opt_files_first = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--readme")) + { + opt_readme = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--html")) + { + opt_html = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--index")) + { + opt_index = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--gpgweb")) + { + opt_gpgweb = opt_html = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--exclude")) + { + argc--; argv++; + if (!argc || !**argv) + die ("argument missing for option '%s'\n", argv[-1]); + sl = xmalloc (sizeof *sl + strlen (*argv)); + strcpy (sl->d, *argv); + sl->next = opt_exclude; + opt_exclude = sl; + argc--; argv++; + } + else if (!strncmp (*argv, "--", 2)) + die ("unknown option '%s' (use --help)\n", *argv); + } + + if (argc < 1 || argc > 2) + die ("usage: " PGMNAME " [options] directory [title]\n"); + + + scan_directory (argv[0], argv[1]? argv[1]:argv[0]); + + + return 0; +} + +/* +Local Variables: +compile-command: "cc -Wall -g -o ftp-indexer ftp-indexer.c" +End: +*/ diff --git a/web/share/site.css b/web/share/site.css index b34693f..c075577 100644 --- a/web/share/site.css +++ b/web/share/site.css @@ -1,758 +1,764 @@ /* site.css * * This code is Copyright 1998--2013 The GnuPG Project and licensed * under a Creative Commons Attribution-ShareAlike 3.0 Unported * License. See the file copying.org for details. */ /* Color names as used by lolo's old site design: * gray #5c6064 * lightblue #d0dce8 * darkpurple #784c6c * lightpurple #f0f0fc * grayedlightpurple #ebebf4 */ body { background: #f0f0fc; font-family: "Proxima Nova Regular","Segoe UI",Roboto,"Droid Sans","Helvetica Neue",Arial,sans-serif; font-weight: 400; height: 100%; } /* div#wrapper { background: transparent url(/share/email-envelope.png) top right no-repeat; } */ div#wrapper, div#footer { max-width: 850px; margin: auto; } h1, h2, h3 { color: #000; font-family: "Helvetica Neue",Arial,sans-serif; font-weight: bold; font-variant: small-caps; letter-spacing: 0.1em; } h1, h2 { font-size: x-large; } h3 { font-size: 1em; } /* Links */ a:link { /* color: #784c6c;*/ font-weight: bold; text-decoration: none; } a:hover { background-color: #d0dce8; font-weight: bold; text-decoration: none; } a:visited { /* color: #5c6064; */ font-weight: bold; text-decoration: none; } a.img:hover { background-color: #f0f0fc; } /* Raise attention */ li.important, span.important { color: red; } div.urgent { width: 85%; text-align: center; border: solid red; font-weight: bold; } .ii { display: none !important; } /* Other elements as commonly used by org-mode */ p { margin-top: 1%; } img { border-width: 0; } img.lfloat { float: left; margin-right: 1em; } img.rfloat { float: right; margin-left: 1em; } /* The figure class is used by the blog entries. We use display to suppress the figure number inserted by org-mode. */ .figure { border: 1px solid #808080; margin: 0 1em 0.5em 0.5em; padding: 0.5em; text-align: center; } /* Use an outer div with this class to move an org figure to the left. Note that org uses a fixed class for a figure and we use this hack to override it. */ div.figure-left { float: left; } div.figure { float: right; margin-right:0 !important; } .figure p { margin: 0; padding: 0.5em 0 0.2em 0; } .figure-number { display: none !important; } .underline { text-decoration: underline; } .example { overflow: auto; } .correction { color: #ff0000; } .postdate { font-weight: normal; font-style: italic; } div.entry-qotd p { padding-left: 20%; margin-bottom: 0; } div.entry-qotd { margin-bottom: 3%; } #kicker { padding-top: 5%; font-size: 125%; border-bottom: 3px #FFb580 solid; font-family: verdana,helvetica; margin-bottom: 2%; } #header { background: transparent; height: 150px; padding: 0px; } #header a.logo img { height: 120px; } #header a.logo:hover img { background: transparent; opacity: 0.4; } #header a, #header a:hover { background: transparent; } #cornerImage { width: 128px; height: 130px; margin-top: -7.5em; margin-left: 82%; padding: 0; padding-right: 2%; } /* Navigation */ /* Reset the link attributes for nav except for hover */ nav a:visited, a:link { color: #757575; font-weight: bold; text-decoration: none; } nav ul, ul ul.sub-menu { margin: 0; padding: 0; z-index: 5; } nav ul { box-shadow: 0px 0px 9px rgba(0,0,0,0.15); padding: 0 20px; border-radius: 10px; background: #efefef; list-style: none; } nav ul li, ul ul.sub-menu li { list-style-type: none; display: inline-block; } /*Link Appearance*/ nav ul li a, ul li ul.sub-menu li a { text-decoration: none; color: #fff; padding: 10px; display:inline-block; } /*Make the parent of sub-menu relative*/ nav ul li { position: relative; } /*sub menu*/ nav ul li ul.sub-menu { display:none; } nav ul li:hover ul.sub-menu { display:block; background: #5f6975; color: #fff; z-index: 99; border-radius: 0px; position: absolute; top: 37px; left: 0; } nav ul ul li { border-bottom: 1px solid #575f6a; position: relative; width: 100%; } nav ul ul li a { display:block !important; } nav ul ul li a:hover { background: #4b545f; } /* The second menu line for stop menu with sub-menus. This is a non-nested list. */ nav.subnav { margin-top: 1.5em; } nav.subnav ul { display: inline-table; list-style: none; color: #757575; position: relative; box-shadow: 0px 0px 5px rgba(0,0,0,0.15); border-radius: 6px; background-color: #efefef; } nav.subnav ul li a { display: block; font-weight: normal; text-decoration: none; padding: 0 30px 0 10px; } nav * li a.selected { color: #0093DD; } /* The bottom menu */ #nav_bottom ul { list-style: none; padding-left: 0; margin-left: 0; float: left; } #nav_bottom li { float: left; padding-right: 3em; } #nav_bottom p { clear: left; padding-top: 1em; } #nav_bottom a { clear: left; font-variant: normal; } /* Other stuff */ main { } main ul { list-style: square; padding-left: 0; margin-left: 1em; } div#content { background-color: #fff; margin-top: 1em; padding: 1em; box-shadow: 0 1px 1px rgba(154,170,207,0.1); } div#content a, div#footer a { color: #0093DD !important; } div.outline-text-2 { margin: 0; padding: 0.5em 0.5em 0 0; border-top: 2px solid #DEECF9; border-right: 2px solid #DEECF9; } div.outline-text-3 { padding-top: 3px; padding-right: 3px; border-top: 1px solid #E2EEFA; border-right: 1px solid #E2EEFA; } div.outline-text-2, div.outline-text-3 { margin-bottom: 2em; } div.outline-text-3, div > h3 { /*margin-left: 1em;*/ } h2, h3 { margin-bottom: 0; padding-bottom: 0; } h3 { font-size: 1em; } /* Not anymore used: #rightColumn { float: right; width: 18%; margin-left: 5%; margin-right: 2%; margin-top: 2%; } #rightColumn ul { list-style: square; padding-left: 0; margin-left: 1em; } */ .morelink { font-size: smaller; font-variant: normal; font-weight: normal; text-decoration: none; } .morelink:after { content: "{more}"; } /* Note that the .footerbox takes care of the padding. */ #cpyright { padding-top: 0em; } #smallnote { font-size: 0.8em; } .smallnote { font-size: 0.8em; } #footer { border-top: 2px solid #5c6064; margin-top: 5em; margin-left: 5%; margin-right: 5%; clear: both; font-size: 0.8em; } #checkoutSummary { background-color: #f0f0f0; } .articleRight { float: right; padding: 2%; } pre { border: thin black solid; background-color: #efefef; padding: 0.5em; overflow: auto; } +pre.ftp-readme { + border: none; + background-color: #ffffff; +} + + /* Classes used by makeinfo (manuals). */ pre.display { font-family:inherit; } pre.format { font-family:inherit; } pre.smalldisplay { font-family:inherit; font-size:smaller; } pre.smallformat { font-family:inherit; font-size:smaller; } pre.smallexample { font-size:smaller; } pre.smalllisp { font-size:smaller; } span.sc { font-variant:small-caps; } span.roman { font-family:serif; font-weight:normal; } span.sansserif { font-family:sans-serif; font-weight:normal; } /* Table related rules as used by org-mode. */ table { margin-left: 5%; } .left { margin-left: 0px; margin-right: auto; text-align: left; } .center { margin-left: auto; margin-right: auto; text-align: center; } .right { margin-left: auto; margin-right: 0px; text-align: right; } th.left { text-align:center; } th.center { text-align:center; } th.right { text-align:center; } th.wideright { text-align:center; padding-left: 25px; } td.left { text-align:left; padding-left: 10px; } td.center { text-align:center; } td.right { text-align:right; padding-right: 10px; } td.wideright { text-align: right; padding-left: 25px; } /* Footnotes for org-mode. */ h2.footnotes { font-size: 100%; } #footnotes { font-size: 75%; } .footnum { font-size: 1em; float: left; } /* Tag cloudlist. */ #tagcloudlist ul { list-style: none; float: left; } #tagcloudlist li { float: left; line-height: 130%; font-variant: small-caps; padding-right: 1em; } #tagcloudlist li:before { content: "\00bb\00a0"; } #tagcloudlist p { clear: left; padding-top: 1em; font-size: 0.8em; } #tagcloudlist p.doclear { clear: left; padding-top: 0; padding-bottom: 0; margin-top: 0; margin-bottom: 0; } #tagcloudlist a { font-variant: normal; font-size: 0.8em; } /* A box of logos. */ .logobox p { margin-top: 20px; } .logobox img { margin-right: 20px; } /* A box used for small graphics at the page bottom. The images are right aligned, a single P is used to clear the alignment. */ .footerbox { margin-top: 12px; margin-bottom: 5px; } .footerbox img { float: right; } .footerbox p { margin-top: 0px; margin-bottom: 0px; clear: both; } /* Used by the list of people. */ .people { float: left; margin-top: 1em; margin-right: 1em; margin-bottom: 1em; min-width: 120px; } /* Forms */ .inputpanel { background-color: #FAEBD7; } /* Donation stuff. */ .buttonbox { margin-top: 20px; margin-bottom: 20px; float: none; } .donate-button { overflow: hidden; display: inline-block; background-image: linear-gradient(#28A0E5, #015E94); /* border: 0px none; */ padding-left: 10px; padding-right: 10px; text-decoration: none; border-radius: 5px; box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.2); cursor: pointer; line-height: 30px; font-size: 14px; font-weight: bold; color: #fff; float: left; margin-right: 20px; margin-bottom: 20px; } .donate-button-low { overflow: hidden; display: inline-block; background-image: linear-gradient(#28A0E5, #015E94); /* border: 0px none;*/ padding-left: 10px; padding-right: 10px; text-decoration: none; border-radius: 4px; box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.2); cursor: pointer; line-height: 20px; font-size: 14px; font-weight: bold; color: #fff; float: left; margin-right: 20px; margin-bottom: 20px; margin-top: 5px; } .donation-progress { border: solid 1px; width: 100%; height: 18px; background-color: #ff0; } .donation-progress p { position: relative; font-size: 14px; top: -18px; left: 0; margin-top: 0; margin-left: 5px; margin-right: 5px; margin-bottom: 0; padding-bottom: 2px; } .donation-progress-bar { background-color: #0a0; } /* EOF */