/* vim:ft=c:cindent:ts=4:sts=4:sw=4:et:fdm=marker
 *
 * File:    dwww-cache.c
 * Purpose: Manage the dwww cache of converted documents.
 * Author:  Lars Wirzenius
 * Description: See the manual page for how to use this program.
 *
 *      Basically, what we do is read in a file from stdin,
 *      store it somewhere, and then retrieve it when asked.
 *      The original name of the file is given on the command
 *      line.  We must be quick, so we keep a mapping between
 *      between the original name and the new name in a file.
 *
 *      The current implementation uses a simple "database"
 *      for the mapping that was written specifically for this
 *      program.  It's relatively fast as long as the database
 *      is not too big -- the whole database is read and written
 *      instead of just a record.  This may have to change
 *      in a future version.  On the other hand, as long as
 *      the database can be kept small, this is quite fast.
 *
 *      One way to make it smaller than it is is to compress
 *      the filenames, since they contain a lot of common
 *      strings ("/usr/share/man/" for example).  We'll see.
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <publib.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#include <utime.h>
#include <sys/file.h>
#include <libgen.h>
#include <stdbool.h>
#include <stdint.h>

#include "utils.h"

#define BUF_SIZE 1024

/*
 * Filename prefixes (for simple compression). At most 256 elements!
 */
static const char *prefs[] = {/* prefs[] {{{*/
    "",     /* MUST BE EMPTY STRING */
    "/usr/lib/",
    "/usr/share/doc/",
    "/usr/share/info/",
    "/usr/share/common-licenses/",
    "/usr/share/man/man1/",
    "/usr/share/man/man2/",
    "/usr/share/man/man3/",
    "/usr/share/man/man4/",
    "/usr/share/man/man5/",
    "/usr/share/man/man6/",
    "/usr/share/man/man7/",
    "/usr/share/man/man8/",
    "/usr/share/",
    "/usr/local/share/doc/",
    "/usr/local/share/info/",
    "/usr/local/share/man/man1/",
    "/usr/local/share/man/man2/",
    "/usr/local/share/man/man3/",
    "/usr/local/share/man/man4/",
    "/usr/local/share/man/man5/",
    "/usr/local/share/man/man6/",
    "/usr/local/share/man/man7/",
    "/usr/local/share/man/man8/",
    "/usr/local/share/"
};/*}}}*/
static int NPREFS = sizeof(prefs) / sizeof(*prefs);

/*
 * Information about one file in in-memory format.
 */
struct cache_entry_header {/*{{{*/
    char     type;
    uint8_t  pref;    /* full name if prefs[pref]+original */
    uint16_t size;    /* size of cached file */
};  /* header of entry, must not contain pointers */

struct cache_entry {
    struct cache_entry_header hdr;
    const char *original;
    const char *converted;
 };/*}}}*/
static const size_t MIN_RAW_ENTRY_SIZE = sizeof(struct cache_entry_header) + sizeof("x") /* min original */ + sizeof("x/1") /* min converted */;


/*
 * The whole database.
 */
struct dochash_header {/*{{{*/
  uint32_t magic_version;
  uint32_t noofentries;
};
struct dochash {
    char*  db; /* first few bytes is dochash_header */
    off_t  db_size;
    struct dynarr tab;
};/*}}}*/
static const uint32_t MAGIC_VERSION = 0xb53cfce7; /* some randomly chosen bytes */

/*
 * Where we store the files and the database.
 */
#define SPOOL_DIR "/var/cache/dwww/db/"

/*
 * The name of the database in SPOOL_DIR.
 */
#define CACHE_DB SPOOL_DIR ".cache_db"


/*
 * What does the user want us to do?
 */
enum {
    action_error,
    action_help,
    action_store,
    action_lookup,
    action_list,
    action_list_all,
    action_clean
};
static int action = action_error;

/*
 * Options; for GNU getopt_long.
 */
static struct option options[] = {/*{{{*/
    { "help", 0, &action, action_help },
    { "store", 0, &action, action_store },
    { "lookup", 0, &action, action_lookup },
    { "list", 0, &action, action_list },
    { "list-all", 0, &action, action_list_all },
    { "clean", 0, &action, action_clean },
    { NULL, 0, NULL, 0 },
};/*}}}*/


static int parse_options(int, char **, struct option *, int *);
static bool store(char *, char *);
static bool lookup(char *, char *);
static bool list(char *, char *);
static bool list_all(char *, char *);
static bool clean(char *, char *);

static bool help(char *type UNUSED, char *location UNUSED) {/*{{{*/
    fprintf(stdout, "Usage: %s [--lookup|--store|--list] type location\n"
                    "       %s --list-all|--clean|--help\n",
                    get_progname(), get_progname() );
    exit(0);
}/*}}}*/

static int open_db_reading(void);
static int open_db_writing(void);
static bool read_db(int, struct dochash *);
static int compare_entry(const void *, const void *);
static void sort_db(struct dochash *);
static int lookup_db(struct dochash *, const char *);
static int insert_db(struct dochash *, struct cache_entry *);
static bool write_db(int, struct dochash *);
static bool close_db(const int, const bool);
static size_t storage_size(const struct cache_entry *);
static bool output_file(char *);
static void init_entry(struct cache_entry *, const char *, const char *, const char *);
static int create_cached_file(char *, char * buf, size_t);
static bool copy_fd_to_fd(int, int, int);
static const char *int_to_type(int);
static char type_to_int(const char *);
static bool rebuild_db(struct dochash *);
static bool is_entry_valid(const struct cache_entry* const entry);
static int find_pref(const char *);
static bool get_orig_stat_data(const char * const name, time_t *);
static bool get_cached_stat_data(const char * const name, uint16_t * size, time_t * mtime);
static bool set_modtime_from_orig(const char * const , const char * const);

int main(int argc, char **argv) {/*{{{*/
    static struct {
        int action;
        int need_args;
        bool (*func)(char *, char *);
    } actions[] = {
        { action_help, 0, help },
        { action_lookup, 2, lookup },
        { action_store, 2, store },
        { action_list, 2, list },
        { action_list_all, 0, list_all },
        { action_clean, 0, clean },
        { action_error, 0, NULL },
    };
    int i, first_nonopt;

    dwww_initialize( "dwww-cache");


    if (parse_options(argc, argv, options, &first_nonopt) == -1)
        return EXIT_FAILURE;

    for (i = 0; actions[i].action != action_error; ++i)
        if (actions[i].action == action)
            break;

    if (actions[i].action == action_error)
        errormsg(1, 0, "error: no action specified");

    if (actions[i].need_args != argc - first_nonopt)
        errormsg(1, 0, "error: wrong number of args");

    if (!actions[i].func(argv[first_nonopt], argv[first_nonopt+1]))
        return EXIT_FAILURE;

    if (fflush(stdout) == EOF || ferror(stdout)) {
        errormsg(0, -1, "error writing to stdout");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}/*}}}*/


static int parse_options(int argc, char **argv, struct option *opt, int *ind) {/*{{{*/
    int o;

    while ((o = getopt_long(argc, argv, "", opt, ind)) != EOF)
        if (o == '?' || o == ':')
            return -1;
    *ind = optind;
    return 0;
}/*}}}*/


static bool print_entry(const struct cache_entry* const entry) {/*{{{*/
    const bool valid = is_entry_valid(entry);

    printf("%s %s%s %s %u %s\n", int_to_type(entry->hdr.type),
           prefs[entry->hdr.pref], entry->original, entry->converted,
           (unsigned)(entry->hdr.size),
           (valid ? "valid" : "outdated"));
    return valid;
}/*}}}*/

static bool list(char *type UNUSED, char *location) {/*{{{*/
    struct dochash hash;
    struct cache_entry *data;
    int i, db;

    db = open_db_reading();
    if (db == -1)
        return false;
    if (!read_db(db, &hash))
        return close_db(db, false);
    i = lookup_db(&hash, location);
    if (i == -1)
        return close_db(db, false);

    data = hash.tab.data;
    const bool result = print_entry(&data[i]);
    return close_db(db, result);
}/*}}}*/



static bool list_all(char *type UNUSED, char *location UNUSED) {/*{{{*/
    struct dochash hash;
    struct cache_entry *data;
    size_t i;
    int    db;

    db = open_db_reading();
    if (db == -1)
        return false;
    if (!read_db(db, &hash))
        return close_db(db, false);
    data = hash.tab.data;

    for (i = 0; i < hash.tab.used; ++i) {
        (void)print_entry(&data[i]);
    }

    return close_db(db, true);
}/*}}}*/



static bool lookup(char *type UNUSED, char *location) {/*{{{*/
    struct dochash hash;
    struct cache_entry *data;
    int i, db;
    char buf[BUF_SIZE];

    db = open_db_reading();
    if (db == -1)
        return false;
    if (!read_db(db, &hash))
        return close_db(db, false);
    i = lookup_db(&hash, location);
    if (i == -1)
        return close_db(db, false);

    data = hash.tab.data;

    bool result = is_entry_valid(&data[i]);
    if (result) {
        snprintf(buf,  sizeof(buf),  "%s%.20s", SPOOL_DIR, data[i].converted);
        result = output_file(buf);
    }
    return close_db(db, result);
}/*}}}*/


static bool internal_store(char * type, /*{{{*/
                           char * location,
                           const char * cached_file,
                           const int fd,
                           const int db) {
    if (!copy_fd_to_fd(STDIN_FILENO, fd, STDOUT_FILENO)) {
        errormsg(0, -1, "can't copy stdin to %s", *cached_file ? cached_file : "stdout");
        return false;
    }

    if (!*cached_file)
        return false;

    struct dochash hash;
    if (!read_db(db, &hash))
        return false;

    char prev_cached_file[BUF_SIZE];
    *prev_cached_file = 0;

    int i = lookup_db(&hash, location);
    if (i != -1) {
        const struct cache_entry *data = hash.tab.data;
        snprintf(prev_cached_file, sizeof(prev_cached_file), "%s%s", SPOOL_DIR, data[i].converted);
    } else {
        struct cache_entry new;
        init_entry(&new, type, location, "");
        i = insert_db(&hash, &new);
        if (i == -1)
            return false;
    }

    struct cache_entry *data = hash.tab.data;
    data[i].converted = cached_file + sizeof(SPOOL_DIR) - 1;
    if (!set_modtime_from_orig(cached_file, location)
        || !get_cached_stat_data(cached_file, &data[i].hdr.size, NULL)
        || !write_db(db, &hash))
        return false;

    if (*prev_cached_file)
        (void) unlink(prev_cached_file);

    return true;
}/*}}}*/


static bool store(char *type, char *location) {/*{{{*/
    int  db, fd;
    bool retval;
    char cached_file[BUF_SIZE];
    *cached_file = 0;

    fd = -1;

    db = open_db_writing();
    if (db >= 0)
        fd = create_cached_file(location, cached_file, sizeof(cached_file));

    if (fd < 0) {
        /* failed to open database or create temporary file, thus
           output to stdout only & don't cache */
        *cached_file = 0;
        fd = STDOUT_FILENO;
    }

    retval = internal_store(type,
                            location,
                            cached_file,
                            fd,
                            db);

    if (!retval && *cached_file && unlink(cached_file) < 0)
        errormsg(0, -1, "can't remove temporary file %s", cached_file);

    if (fd != STDOUT_FILENO && close(fd) < 0) {
        errormsg(0, -1, "can't close file");
        retval = false;
    }

    if (db >= 0)
        return close_db(db, retval);

    return retval;
}/*}}}*/



static bool clean(char *type UNUSED, char *location UNUSED) {/*{{{*/
    struct  dochash hash;
    char    buf[BUF_SIZE];

    const int db = open_db_writing();
    if (db == -1 || !read_db(db, &hash))
        return false;

    struct cache_entry* data = hash.tab.data;
    size_t i = 0, j = 0;
    do {
        for (i = 0, j = 0; i < hash.tab.used; ++i) {
            if (is_entry_valid(&data[i]))
                data[j++] = data[i];
            else {
                snprintf(buf,  sizeof(buf),  "%s%.20s", SPOOL_DIR, data[i].converted);
                (void) unlink(buf);
            }
        }
        hash.tab.used = j;
    /* Continue cleaning until no changes are done in order to remove entries which
       share to the same cache file, what might happen after cached file is (possibly
       externally) removed, and then `dwww-cache --store' assigns the same cached file
       name to a different entry */
    } while (i != j);

    /* In case of error try truncating or unlinking the database */
    const bool result = write_db(db, &hash);
    if (!result && ftruncate(db, 0) < 0) {
        errormsg(0, -1, "failed to save or truncate database");
        (void) unlink(CACHE_DB);
    }

    return close_db(db, result);
}/*}}}*/


static int open_db_reading(void) {/*{{{*/
    int db;
    int tries = 5;

    db = open(CACHE_DB, O_RDONLY, 0644);
    if (db == -1) {
        if (errno != ENOENT)
        errormsg(0, -1, "can't check cache database");
        return -1;
    }

    while (--tries && flock(db, LOCK_SH | LOCK_NB) < 0)
        sleep(1);

    if (!tries) {
        (void) close(db);
        errormsg(0, -1, "can't create shared lock on cache database");
        return -1;
    }

    return db;
}/*}}}*/


static int open_db_writing(void) {/*{{{*/

    const int db = open(CACHE_DB, O_RDWR | O_CREAT, 0664);
    if (db == -1) {
        errormsg(0, -1, "can't update cache database");
        return -1;
    }

    int tries = 5;
    while (--tries && flock(db, LOCK_EX | LOCK_NB) < 0)
            sleep(1);

    if (!tries) {
        (void) close(db);
        errormsg(0, -1, "can't create exclusive lock on cache database");
        return -1;
    }

    return db;
}/*}}}*/


static bool read_db(int db, struct dochash *hash) {/*{{{*/
    struct stat st;
    char *p, *end;
    struct cache_entry *data;

    memset(hash, 0, sizeof(*hash));
    dynarr_init(&hash->tab, sizeof(struct cache_entry));

    if (fstat(db, &st) == -1) {
        errormsg(0, -1, "can't find database size");
        return false;
    }
    const size_t file_size = st.st_size;

    p = malloc(file_size);
    if (p == NULL) {
        errormsg(0, -1, "out of memory");
        return false;
    }

    if (read(db, p, file_size) != (ssize_t)file_size) {
        free(p);
        errormsg(0, -1, "couldn't read database");
        return false;
    }

    if (file_size < sizeof(struct dochash_header)) {
        free(p);
        if (file_size) /* file might got truncated by clean() */
            errormsg(0, 0, "warning: database file is too short: %u", file_size);
        return true;
    }

    const struct dochash_header hdr = *(struct dochash_header*)(p);
    if (hdr.magic_version != MAGIC_VERSION) {
        free(p);
        errormsg(0, 0, "warning: invalid format of database file: %x, expected: %x",
                 hdr.magic_version, MAGIC_VERSION);
        return true;
    }

    if (!hdr.noofentries) {
        free(p);
        return true;
    }

    if (p[file_size-1]) { /* the file needs to end with a null byte */
        free(p);
        errormsg(0, 0, "warning: invalid database file",
                 hash->tab.used, hdr.noofentries);
        return true;
    }

    if (dynarr_resize(&hash->tab, hdr.noofentries) == -1) {
        free(p);
        errormsg(0, -1, "out of memory");
        return false;
    }

    hash->db = p;
    hash->db_size = file_size;
    p += sizeof(hdr);
    end = p + file_size;
    while (p + MIN_RAW_ENTRY_SIZE < end && hash->tab.used < hdr.noofentries) {
        data = hash->tab.data;
        data += hash->tab.used;
        memcpy((char*) &data->hdr, p, sizeof(data->hdr));
        p += sizeof(data->hdr);
        data->original = p;
        p += strlen(p) + 1;
        data->converted = p;
        p += strlen(p) + 1;
        if (data->hdr.pref < NPREFS)
          ++hash->tab.used;
    }

    if (hash->tab.used != hdr.noofentries)
        errormsg(0, 0, "warning: invalid number of entries: %u, expected: %u",
                 hash->tab.used, hdr.noofentries);

    return true;
}/*}}}*/


static int compare_entry(const void *a, const void *b) {/*{{{*/
    const struct cache_entry *aa = a;
    const struct cache_entry *bb = b;

    int i = aa->hdr.pref - bb->hdr.pref;
    if (i == 0)
        i = strcmp(aa->original, bb->original);
    return i;
}/*}}}*/


static void sort_db(struct dochash *hash) {/*{{{*/
    qsort(hash->tab.data, hash->tab.used, sizeof(struct cache_entry),
        compare_entry);
}/*}}}*/


static int lookup_db(struct dochash *hash, const char *original) {/*{{{*/
    struct cache_entry dummy, *p, *data;

    init_entry(&dummy, "", original, 0);
    data = hash->tab.data;
    p = bsearch(&dummy, data, hash->tab.used,
        sizeof(struct cache_entry), compare_entry);
    if (p == NULL)
        return -1;
    if (compare_entry(&dummy, p) != 0) {
        errormsg(0, 0, "warning: cache is not sorted properly");
        return -1;
    }
    return p - data;
}/*}}}*/


static int insert_db(struct dochash *hash, struct cache_entry *new) {/*{{{*/
    struct cache_entry *data;
    int i;

    i = lookup_db(hash, new->original);
    if (i == -1) {
        if (dynarr_resize(&hash->tab, hash->tab.used + 1) == -1) {
            errormsg(0, -1, "out of memory");
            return i;
        }
        i = (int) hash->tab.used;
        ++hash->tab.used;
    }

    data = hash->tab.data;
    data[i] = *new;
    return i;
}/*}}}*/


static bool rebuild_db(struct dochash *hash) {/*{{{*/
    struct  cache_entry *data;
    size_t  i, n;
    size_t  newdbsize;
    char*   newdb;
    char    *qq;

    if (!hash->tab.used) {
        free(hash->db);
        hash->db = NULL;
        return true;
    }

    sort_db(hash);

    data = hash->tab.data;
    newdbsize = sizeof(struct dochash_header);
    for (i = 0; i < hash->tab.used; ++i)
        newdbsize += storage_size(data + i);

    newdb = malloc(newdbsize + 1);
    if (newdb == NULL) {
        errormsg(0, -1, "out of memory");
        return false;
    }
    struct dochash_header* hdr = (struct dochash_header*)newdb;
    hdr->magic_version = MAGIC_VERSION;
    hdr->noofentries = hash->tab.used;

    qq = newdb + sizeof(struct dochash_header);

    for (i = 0; i < hash->tab.used; ++i) {
        memcpy(qq, (char*) &data[i].hdr, sizeof(data[i].hdr));
        qq += sizeof(data[i].hdr);
        n = strlen(data[i].original) + 1;
        memcpy(qq, data[i].original, n);
        qq += n;
        n = strlen(data[i].converted) + 1;
        memcpy(qq, data[i].converted, n);
        qq += n;
    }

    free(hash->db);
    hash->db = newdb;
    hash->db_size = newdbsize;

    return true;
}/*}}}*/


static bool write_db(int db, struct dochash *hash) {/*{{{*/
    if (!rebuild_db(hash))
       return false;

    if (lseek(db, 0, SEEK_SET) == -1) {
        errormsg(0, -1, "can't update cache database");
        return false;
    }

    if (ftruncate(db, 0) == -1) {
        errormsg(0, -1, "can't truncate cache database");
        return false;
    }

    if (hash->db && write(db, hash->db, hash->db_size) != hash->db_size) {
        errormsg(0, -1, "can't update cache database");
        return false;
    }

    return true;
}/*}}}*/


static bool close_db(int db, const bool retVal) {/*{{{*/
    if (flock(db, LOCK_UN) == -1) {
        close(db);
        errormsg(0, -1, "can't unlock cache database");
        return false;
    }
    if (close(db) == -1) {
        errormsg(0, -1, "error closing database");
        return false;
    }
    return retVal;
}/*}}}*/


static size_t storage_size(const struct cache_entry *p) {/*{{{*/
    return sizeof(p->hdr) + strlen(p->original) + 1 + strlen(p->converted) + 1;
}/*}}}*/


static void init_entry(struct cache_entry *p, const char *type, const char *orig,/*{{{*/
const char *conv) {
    memset(p, 0, sizeof(*p));
    p->hdr.type = type_to_int(type);
    p->hdr.pref = find_pref(orig);
    p->original = orig + strlen(prefs[p->hdr.pref]);
    p->converted = conv;
}/*}}}*/


static bool output_file(char *name) {/*{{{*/

    const int fd = open(name, O_RDONLY);
    if (fd == -1) {
        errormsg(0, -1, "can't read file %s", name);
        return false;
    }

    const bool result = copy_fd_to_fd(fd, STDOUT_FILENO, STDOUT_FILENO);
    (void) close(fd);
    return  result;
}/*}}}*/


static bool copy_fd_to_fd(int from, int to_prim, int to_sec) {/*{{{*/
    char    buf[BUF_SIZE];
    ssize_t n;
    ssize_t n_prim = 0;
    ssize_t n_sec  = 0;


    if (to_prim == to_sec)
        n_sec = -100; /* *must* be less then 0 */

    while ((n = read(from, buf, sizeof(buf))) > 0) {
        if (n_prim >= 0)
            n_prim = write(to_prim, buf, (size_t)n) - n;
        if (n_sec >= 0)
            n_sec = write(to_sec, buf, (size_t)n) - n;
        if (n_prim < 0 && n_sec < 0) {
            errormsg(0, -1, "error writing");
            return false;
        }
    }

    if (n_prim < 0) {
        errormsg(0, -1, "error writing");
        return false;
    }

    if (n < 0) {
        errormsg(0, -1, "error reading");
        return false;
    }

    return true;
}/*}}}*/

static int create_cached_file(char * location, char * buf, size_t buf_size) {/*{{{*/
    const char * const tmp = basename(location);

    snprintf(buf, buf_size, SPOOL_DIR "%c", tmp ? *tmp : '_');
    if (mkdir(buf, 0775) == -1 && errno != EEXIST) {
        errormsg(0, -1, "can't create directory %s", buf);
        return -1;
    }
    char * loc = buf + sizeof(SPOOL_DIR);
    const size_t loc_size = buf_size - sizeof(SPOOL_DIR);
    struct timeval tv;
    gettimeofday(&tv, NULL);
    uint32_t val = ((uint32_t)tv.tv_usec << 8) ^ tv.tv_sec ^ getpid();
    val &= (uint32_t)(0xFFFFF);
    for (int i = 0; i < 10000; ++i, val += 99) {
        snprintf(loc, loc_size, "/%x", val);
        const int fd = open(buf, O_CREAT | O_EXCL | O_RDWR, 0664);
        if (fd > -1)
            return fd;
        if (errno != EEXIST)
            break;
    }
    errormsg(0, -1, "can't create output file, last tried one was %s", buf);
    return -1;
}/*}}}*/

static const struct {/*typetab[] {{{*/
    const char *str;
    char ch;
} typetab[] = {
    { "man", 'm' },
    { "runman", 'r' },
    { "info", 'i' },
    { "text", 't' },
    { "text/plain", 't' },
    { "text/css", 't' },
    { "dir", 'd' },
    { "html", 'h' },
    { "text/html", 'h' },
    { "file", 'f' },
    { NULL, '?' },
};/*}}}*/


static char type_to_int(const char *str) {/*{{{*/
    int i;

    for (i = 0; typetab[i].str != NULL; ++i)
        if (strcmp(typetab[i].str, str) == 0)
            break;
    return typetab[i].ch;
}/*}}}*/


static const char *int_to_type(int ch) {/*{{{*/
    int i;

    for (i = 0; typetab[i].str != NULL; ++i)
        if (typetab[i].ch == ch)
            return typetab[i].str;
    return "unknown";
}/*}}}*/


static bool get_orig_stat_data(const char * const name, time_t * mtime) {/*{{{*/
    struct stat st;
    if (stat(name, &st) == -1)
        return false;
    if (!st.st_size) /* file is empty, possibly truncated before */
        return false;
    *mtime = st.st_mtime;
    return true;
}/*}}}*/


static bool get_cached_stat_data(const char * const name, uint16_t * size, time_t * mtime) {/*{{{*/
    struct stat st;
    if (stat(name, &st) == -1 || !st.st_size)
        return false;
    *size = (uint16_t) st.st_size;
    if (mtime)
        *mtime = st.st_mtime;
    return true;
}/*}}}*/

static bool is_entry_valid(const struct cache_entry* const entry) {/*{{{*/

    char origname[BUF_SIZE];
    char cachedname[BUF_SIZE];
    snprintf(origname, sizeof(origname), "%s%s", prefs[entry->hdr.pref], entry->original);
    snprintf(cachedname,  sizeof(cachedname),  "%s%.20s", SPOOL_DIR, entry->converted);

    time_t   cachedmtime;
    uint16_t cachedsize;
    if (!get_cached_stat_data(cachedname,  &cachedsize, &cachedmtime)
        || cachedsize != entry->hdr.size) {
      /* cached file does not exist or is empty or was modified after storing */
        return false;
    }

    time_t origmtime;
    if (get_orig_stat_data(origname, &origmtime)
        && origmtime == cachedmtime)
        return true;

    /* Don't care if truncate fails or not... It could fail mostly because of EPERM, but it's OK
       Normally I would write
          (void) truncate(cachedname, 0);
       but compilers try to be too smart, see
       https://launchpadlibrarian.net/28344578/buildlog_ubuntu-karmic-amd64.dwww_1.11.1ubuntu1_FULLYBUILT.txt.gz
   */
    if (!truncate(cachedname, 0))
       return false;
    return false;
}/*}}}*/


static int find_pref(const char *s) {/*{{{*/
    int i;
    for (i = 1; i < NPREFS; ++i)
        if (strncmp(s, prefs[i], strlen(prefs[i])) == 0)
            return i;
    return 0;
}/*}}}*/

static bool set_modtime_from_orig(const char * const cachedname, /*{{{*/
                                  const char * const origname) {
    struct utimbuf ut;
    time_t mtime;

    if (!get_orig_stat_data(origname, &mtime))
        return false;

    ut.modtime = mtime;
    ut.actime  = time(NULL);

    return utime(cachedname, &ut) >= 0;
}/*}}}*/
