/*
**  Storage manager module for timehash method.
**
**  Written by Clayton O'Neill in 1997.
**
**  Various bug fixes, code and documentation improvements since then
**  in 1997-2010, 2015, 2017, 2021, 2022, 2024, 2026.
*/

#include "portable/system.h"

#include "portable/mmap.h"
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <time.h>

#include "inn/innconf.h"
#include "inn/libinn.h"
#include "inn/messages.h"
#include "inn/paths.h"
#include "inn/wire.h"
#include "methods.h"
#include "timehash.h"

typedef struct {
    char *base;  /* Base of the mmaped file */
    int len;     /* Length of the file */
    DIR *top;    /* Open handle on the top level directory */
    DIR *sec;    /* Open handle on the 2nd level directory */
    DIR *ter;    /* Open handle on the third level directory */
    DIR *artdir; /* Open handle on the article directory */
    struct dirent
        *topde; /* dirent entry for the last entry retrieved in top */
    struct dirent
        *secde; /* dirent entry for the last entry retrieved in sec */
    struct dirent
        *terde; /* dirent entry for the last entry retrieved in ter */
} PRIV_TIMEHASH;

typedef enum {
    FIND_DIR,
    FIND_ART,
    FIND_TOPDIR
} FINDTYPE;

static int SeqNum = 0;

/*
**  The token is @02nnaabbccddyyyy00000000000000000000@
**  where "02" is the timehash method number,
**  "nn" the hexadecimal value of the storage class,
**  "aabbccdd" the arrival time in hexadecimal,
**  "yyyy" the hexadecimal sequence number seqnum.
**
**  innconf->patharticles + '/time-nn/bb/cc/yyyy-aadd'
**  where "nn" is the hexadecimal value of the storage class,
**  "aabbccdd" the arrival time in hexadecimal,
**  "yyyy" the hexadecimal sequence number seqnum.
*/
char *
timehash_explaintoken(const TOKEN token)
{
    char *text;
    uint32_t arrival;
    uint16_t seqnum;

    memcpy(&arrival, &token.token[0], sizeof(arrival));
    memcpy(&seqnum, &token.token[4], sizeof(seqnum));

    xasprintf(&text,
              "method=timehash class=%u time=%lu seqnum=%lu "
              "file=%s/time-%02x/%02x/%02x/%04x-%02x%02x",
              (unsigned int) token.class, (unsigned long) ntohl(arrival),
              (unsigned long) ntohs(seqnum), innconf->patharticles,
              token.class, (ntohl(arrival) >> 16) & 0xff,
              (ntohl(arrival) >> 8) & 0xff, (unsigned int) ntohs(seqnum),
              (ntohl(arrival) >> 24) & 0xff, ntohl(arrival) & 0xff);

    return text;
}

static TOKEN
MakeToken(time_t now, unsigned int seqnum, STORAGECLASS class, TOKEN *oldtoken)
{
    TOKEN token;
    uint32_t i;
    uint16_t s;

    if (oldtoken == (TOKEN *) NULL)
        memset(&token, '\0', sizeof(token));
    else
        memcpy(&token, oldtoken, sizeof(token));
    token.type = TOKEN_TIMEHASH;
    token.class = class;
    i = htonl(now);
    memcpy(token.token, &i, sizeof(i));
    s = htons(seqnum & 0xffff);
    memcpy(&token.token[sizeof(i)], &s, sizeof(s));
    return token;
}

static void
BreakToken(TOKEN token, time_t *now, unsigned int *seqnum)
{
    uint32_t i;
    uint16_t s = 0;

    memcpy(&i, token.token, sizeof(i));
    memcpy(&s, &token.token[sizeof(i)], sizeof(s));
    *now = ntohl(i);
    *seqnum = (unsigned int) ntohs(s);
}

static char *
MakePath(time_t now, unsigned int seqnum, const STORAGECLASS class)
{
    char *path;
    size_t length;

    length = strlen(innconf->patharticles) + 32;
    path = xmalloc(length);
    snprintf(path, length, "%s/time-%02x/%02x/%02x/%04x-%04x",
             innconf->patharticles, class, (unsigned int) ((now >> 16) & 0xff),
             (unsigned int) ((now >> 8) & 0xff), seqnum,
             (unsigned int) ((now & 0xff) | ((now >> 16 & 0xff00))));
    return path;
}

static TOKEN *
PathToToken(char *path)
{
    int n;
    unsigned int tclass, t1, t2, t3, seqnum;
    STORAGECLASS class;
    time_t now;
    static TOKEN token;

    n = sscanf(path, "time-%02x/%02x/%02x/%04x-%04x", &tclass, &t1, &t2,
               &seqnum, &t3);
    if (n != 5) {
        return (TOKEN *) NULL;
    }
    now = ((t1 << 16) & 0xff0000) | ((t2 << 8) & 0xff00)
          | ((t3 << 16) & 0xff000000) | (t3 & 0xff);
    class = tclass;
    token = MakeToken(now, seqnum, class, (TOKEN *) NULL);
    return &token;
}

bool
timehash_init(SMATTRIBUTE *attr)
{
    if (attr == NULL) {
        warn("timehash: attr is NULL");
        SMseterror(SMERR_INTERNAL, "attr is NULL");
        return false;
    }
    attr->selfexpire = false;
    attr->expensivestat = true;
    if (STORAGE_TOKEN_LENGTH < 6) {
        warn("timehash: token length is less than six bytes");
        SMseterror(SMERR_TOKENSHORT, NULL);
        return false;
    }
    return true;
}

TOKEN
timehash_store(const ARTHANDLE article, const STORAGECLASS class)
{
    char *path = NULL;
    char *p;
    time_t now;
    TOKEN token;
    int fd = 0;
    ssize_t result;
    int seq = 0;
    int i;

    if (article.arrived == (time_t) 0)
        now = time(NULL);
    else
        now = article.arrived;

    memset(&token, 0, sizeof(token));

    for (i = 0; i < 0x10000; i++) {
        seq = SeqNum;
        SeqNum = (SeqNum + 1) & 0xffff;
        path = MakePath(now, seq, class);

        if ((fd = open(path, O_CREAT | O_EXCL | O_WRONLY, ARTFILE_MODE)) < 0) {
            if (errno == EEXIST)
                continue;
            p = strrchr(path, '/');
            *p = '\0';
            if (!MakeDirectory(path, true)) {
                syswarn("timehash: could not make directory %s", path);
                token.type = TOKEN_EMPTY;
                free(path);
                SMseterror(SMERR_UNDEFINED, NULL);
                return token;
            } else {
                *p = '/';
                if ((fd =
                         open(path, O_CREAT | O_EXCL | O_WRONLY, ARTFILE_MODE))
                    < 0) {
                    SMseterror(SMERR_UNDEFINED, NULL);
                    syswarn("timehash: could not create %s", path);
                    token.type = TOKEN_EMPTY;
                    free(path);
                    return token;
                }
            }
        }
        break;
    }
    if (i == 0x10000) {
        SMseterror(SMERR_UNDEFINED, NULL);
        warn("timehash: all sequence numbers for time %lu and class %d are"
             " reserved",
             (unsigned long) now, class);
        token.type = TOKEN_EMPTY;
        free(path);
        return token;
    }

    result = xwritev(fd, article.iov, article.iovcnt);
    if (result != (ssize_t) article.len) {
        SMseterror(SMERR_UNDEFINED, NULL);
        syswarn("timehash: error writing %s", path);
        close(fd);
        token.type = TOKEN_EMPTY;
        unlink(path);
        free(path);
        return token;
    }
    close(fd);
    free(path);
    return MakeToken(now, seq, class, article.token);
}

static ARTHANDLE *
OpenArticle(const char *path, RETRTYPE amount)
{
    int fd;
    PRIV_TIMEHASH *private;
    char *p;
    struct stat sb;
    ARTHANDLE *art;

    if (amount == RETR_STAT) {
        if (access(path, R_OK) < 0) {
            SMseterror(SMERR_UNDEFINED, NULL);
            return NULL;
        }
        art = xmalloc(sizeof(ARTHANDLE));
        art->type = TOKEN_TIMEHASH;
        art->data = NULL;
        art->len = 0;
        art->private = NULL;
        return art;
    }

    if ((fd = open(path, O_RDONLY)) < 0) {
        SMseterror(SMERR_UNDEFINED, NULL);
        return NULL;
    }

    art = xmalloc(sizeof(ARTHANDLE));
    art->type = TOKEN_TIMEHASH;

    if (fstat(fd, &sb) < 0) {
        SMseterror(SMERR_UNDEFINED, NULL);
        syswarn("timehash: could not fstat article");
        close(fd);
        free(art);
        return NULL;
    }
    if (sb.st_size < 0 || sb.st_size > INT_MAX) {
        SMseterror(SMERR_UNDEFINED, "article is too large");
        close(fd);
        free(art);
        return NULL;
    }

    private = xmalloc(sizeof(PRIV_TIMEHASH));
    art->private = (void *) private;
    private->len = sb.st_size;
    if (innconf->articlemmap) {
        if ((private->base =
                 mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0))
            == MAP_FAILED) {
            SMseterror(SMERR_UNDEFINED, NULL);
            syswarn("timehash: could not mmap article");
            close(fd);
            free(art->private);
            free(art);
            return NULL;
        }
        if (amount == RETR_ALL)
            madvise(private->base, sb.st_size, MADV_WILLNEED);
        else
            madvise(private->base, sb.st_size, MADV_SEQUENTIAL);
    } else {
        private->base = xmalloc(private->len);
        if (xread(fd, private->base, private->len) < 0) {
            SMseterror(SMERR_UNDEFINED, NULL);
            syswarn("timehash: could not read article");
            close(fd);
            free(private->base);
            free(art->private);
            free(art);
            return NULL;
        }
    }
    close(fd);

    private->top = NULL;
    private->sec = NULL;
    private->ter = NULL;
    private->artdir = NULL;
    private->topde = NULL;
    private->secde = NULL;
    private->terde = NULL;

    if (amount == RETR_ALL) {
        art->data = private->base;
        art->len = private->len;
        return art;
    }

    if ((p = wire_findbody(private->base, private->len)) == NULL) {
        SMseterror(SMERR_NOBODY, NULL);
        if (innconf->articlemmap)
            munmap(private->base, private->len);
        else
            free(private->base);
        free(art->private);
        free(art);
        return NULL;
    }

    if (amount == RETR_HEAD) {
        art->data = private->base;
        art->len = p - private->base;
        /* Headers end just before the first empty line (\r\n). */
        art->len = art->len - 2;
        return art;
    }

    if (amount == RETR_BODY) {
        art->data = p;
        art->len = private->len - (p - private->base);
        return art;
    }
    SMseterror(SMERR_UNDEFINED, "Invalid retrieve request");
    if (innconf->articlemmap)
        munmap(private->base, private->len);
    else
        free(private->base);
    free(art->private);
    free(art);
    return NULL;
}

ARTHANDLE *
timehash_retrieve(const TOKEN token, const RETRTYPE amount)
{
    time_t now;
    unsigned int seqnum;
    char *path;
    ARTHANDLE *art;
    static TOKEN ret_token;

    if (token.type != TOKEN_TIMEHASH) {
        SMseterror(SMERR_INTERNAL, NULL);
        return NULL;
    }

    BreakToken(token, &now, &seqnum);
    path = MakePath(now, seqnum, token.class);
    if ((art = OpenArticle(path, amount)) != (ARTHANDLE *) NULL) {
        art->arrived = now;
        ret_token = token;
        art->token = &ret_token;
    }
    free(path);
    return art;
}

void
timehash_freearticle(ARTHANDLE *article)
{
    PRIV_TIMEHASH *private;

    if (!article)
        return;

    if (article->private) {
        private = (PRIV_TIMEHASH *) article->private;
        if (innconf->articlemmap)
            munmap(private->base, private->len);
        else
            free(private->base);
        if (private->top)
            closedir(private->top);
        if (private->sec)
            closedir(private->sec);
        if (private->ter)
            closedir(private->ter);
        if (private->artdir)
            closedir(private->artdir);
        free(private);
    }
    free(article);
}

bool
timehash_cancel(TOKEN token)
{
    time_t now;
    unsigned int seqnum;
    char *path;
    int result;

    BreakToken(token, &now, &seqnum);
    path = MakePath(now, seqnum, token.class);
    result = unlink(path);
    free(path);
    if (result < 0) {
        SMseterror(SMERR_UNDEFINED, NULL);
        return false;
    }
    return true;
}

static struct dirent *
FindDir(DIR *dir, FINDTYPE type)
{
    struct dirent *de;

    while ((de = readdir(dir)) != NULL) {
        if (type == FIND_TOPDIR)
            if ((strlen(de->d_name) == 7)
                && (strncmp(de->d_name, "time-", 5) == 0)
                && isxdigit((unsigned char) de->d_name[5])
                && isxdigit((unsigned char) de->d_name[6]))
                return de;

        if (type == FIND_DIR)
            if ((strlen(de->d_name) == 2)
                && isxdigit((unsigned char) de->d_name[0])
                && isxdigit((unsigned char) de->d_name[1]))
                return de;

        if (type == FIND_ART)
            if ((strlen(de->d_name) == 9)
                && isxdigit((unsigned char) de->d_name[0])
                && isxdigit((unsigned char) de->d_name[1])
                && isxdigit((unsigned char) de->d_name[2])
                && isxdigit((unsigned char) de->d_name[3])
                && isxdigit((unsigned char) de->d_name[5])
                && isxdigit((unsigned char) de->d_name[6])
                && isxdigit((unsigned char) de->d_name[7])
                && isxdigit((unsigned char) de->d_name[8])
                && (de->d_name[4] == '-'))
                return de;
    }

    return NULL;
}

ARTHANDLE *
timehash_next(ARTHANDLE *article, const RETRTYPE amount)
{
    PRIV_TIMEHASH priv;
    PRIV_TIMEHASH *newpriv;
    char *path;
    struct dirent *de;
    ARTHANDLE *art;
    unsigned int seqnum;
    size_t length;
    TOKEN *nexttoken;

    length = strlen(innconf->patharticles) + 32;
    path = xmalloc(length);
    if (article == NULL || article->private == NULL) {
        if (article != NULL)
            free(article);
        priv.top = NULL;
        priv.sec = NULL;
        priv.ter = NULL;
        priv.artdir = NULL;
        priv.topde = NULL;
        priv.secde = NULL;
        priv.terde = NULL;
    } else {
        priv = *(PRIV_TIMEHASH *) article->private;
        free(article->private);
        free(article);
        if (priv.base != NULL) {
            if (innconf->articlemmap)
                munmap(priv.base, priv.len);
            else
                free(priv.base);
        }
    }

    while (!priv.artdir || ((de = FindDir(priv.artdir, FIND_ART)) == NULL)) {
        if (priv.artdir) {
            closedir(priv.artdir);
            priv.artdir = NULL;
        }
        while (!priv.ter
               || ((priv.terde = FindDir(priv.ter, FIND_DIR)) == NULL)) {
            if (priv.ter) {
                closedir(priv.ter);
                priv.ter = NULL;
            }
            while (!priv.sec
                   || ((priv.secde = FindDir(priv.sec, FIND_DIR)) == NULL)) {
                if (priv.sec) {
                    closedir(priv.sec);
                    priv.sec = NULL;
                }
                if (!priv.top
                    || ((priv.topde = FindDir(priv.top, FIND_TOPDIR))
                        == NULL)) {
                    if (priv.top) {
                        /* end of search */
                        closedir(priv.top);
                        priv.top = NULL;
                        free(path);
                        return NULL;
                    }
                    snprintf(path, length, "%s", innconf->patharticles);
                    if ((priv.top = opendir(path)) == NULL) {
                        SMseterror(SMERR_UNDEFINED, NULL);
                        free(path);
                        return NULL;
                    }
                    if ((priv.topde = FindDir(priv.top, FIND_TOPDIR))
                        == NULL) {
                        SMseterror(SMERR_UNDEFINED, NULL);
                        closedir(priv.top);
                        free(path);
                        return NULL;
                    }
                }
                snprintf(path, length, "%s/%s", innconf->patharticles,
                         priv.topde->d_name);
                if ((priv.sec = opendir(path)) == NULL)
                    continue;
            }
            snprintf(path, length, "%s/%s/%s", innconf->patharticles,
                     priv.topde->d_name, priv.secde->d_name);
            if ((priv.ter = opendir(path)) == NULL)
                continue;
        }
        snprintf(path, length, "%s/%s/%s/%s", innconf->patharticles,
                 priv.topde->d_name, priv.secde->d_name, priv.terde->d_name);
        if ((priv.artdir = opendir(path)) == NULL)
            continue;
    }
    if (de == NULL)
        return NULL;
    snprintf(path, length, "%s/%s/%s/%s/%s", innconf->patharticles,
             priv.topde->d_name, priv.secde->d_name, priv.terde->d_name,
             de->d_name);

    art = OpenArticle(path, amount);
    if (art == (ARTHANDLE *) NULL) {
        art = xmalloc(sizeof(ARTHANDLE));
        art->type = TOKEN_TIMEHASH;
        art->data = NULL;
        art->len = 0;
        art->private = xmalloc(sizeof(PRIV_TIMEHASH));
        newpriv = (PRIV_TIMEHASH *) art->private;
        newpriv->base = NULL;
    } else if (art->private == NULL) {
        art->private = xcalloc(1, sizeof(PRIV_TIMEHASH));
    }
    newpriv = (PRIV_TIMEHASH *) art->private;
    newpriv->top = priv.top;
    newpriv->sec = priv.sec;
    newpriv->ter = priv.ter;
    newpriv->artdir = priv.artdir;
    newpriv->topde = priv.topde;
    newpriv->secde = priv.secde;
    newpriv->terde = priv.terde;
    snprintf(path, length, "%s/%s/%s/%s", priv.topde->d_name,
             priv.secde->d_name, priv.terde->d_name, de->d_name);
    nexttoken = PathToToken(path);
    if (nexttoken == (TOKEN *) NULL) {
        free(path);
        return NULL;
    }
    art->token = nexttoken;
    BreakToken(*art->token, &(art->arrived), &seqnum);
    free(path);
    return art;
}

bool
timehash_ctl(PROBETYPE type, TOKEN *token UNUSED, void *value)
{
    struct artngnum *ann;

    switch (type) {
    case SMARTNGNUM:
        if ((ann = (struct artngnum *) value) == NULL)
            return false;
        /* make SMprobe() call timehash_retrieve() */
        ann->artnum = 0;
        return true;
    default:
        return false;
    }
}

bool
timehash_flushcacheddata(FLUSHTYPE type UNUSED)
{
    return true;
}

void
timehash_printfiles(FILE *file, TOKEN token, char **xref UNUSED,
                    int ngroups UNUSED)
{
    time_t now;
    unsigned int seqnum;
    char *path;

    BreakToken(token, &now, &seqnum);
    path = MakePath(now, seqnum, token.class);
    fprintf(file, "%s\n", path);
}

void
timehash_shutdown(void)
{
}
