/*
   Virtual File System: GNU Tar file system.

   Copyright (C) 1995-2025
   Free Software Foundation, Inc.

   Written by:
   Andrew Borodin <aborodin@vmail.ru>, 2023

   This file is part of the Midnight Commander.

   The Midnight Commander 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.

   The Midnight Commander 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 <https://www.gnu.org/licenses/>.
 */

/**
 * \file
 * \brief Source: Virtual File System: GNU Tar file system
 */

#include <config.h>

#include <ctype.h>  // isdigit()
#include <stdlib.h>
#include <string.h>

#include "lib/global.h"
#include "lib/util.h"  // MC_PTR_FREE

#include "tar-internal.h"

/*** global variables ****************************************************************************/

/*** file scope macro definitions ****************************************************************/

#define XHDR_PROTECTED 0x01
#define XHDR_GLOBAL    0x02

/*** file scope type declarations ****************************************************************/

/* General Interface */

/* Since tar VFS is read-only, inplement decodes only */
struct xhdr_tab
{
    const char *keyword;
    gboolean (*decoder) (struct tar_stat_info *st, const char *keyword, const char *arg,
                         size_t size);
    int flags;
};

/* Keyword options */
struct keyword_item
{
    char *pattern;
    char *value;
};

enum decode_record_status
{
    decode_record_ok,
    decode_record_finish,
    decode_record_fail
};

/*** forward declarations (file scope functions) *************************************************/

static gboolean dummy_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
static gboolean atime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
static gboolean gid_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                             size_t size);
#if 0
static gboolean gname_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
#endif
static gboolean linkpath_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                                  size_t size);
static gboolean mtime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
static gboolean ctime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
static gboolean path_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                              size_t size);
static gboolean size_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                              size_t size);
static gboolean uid_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                             size_t size);
#if 0
static gboolean uname_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                               size_t size);
#endif
static gboolean sparse_path_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                                     size_t size);
static gboolean sparse_major_decoder (struct tar_stat_info *st, const char *keyword,
                                      const char *arg, size_t size);
static gboolean sparse_minor_decoder (struct tar_stat_info *st, const char *keyword,
                                      const char *arg, size_t size);
static gboolean sparse_size_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                                     size_t size);
static gboolean sparse_numblocks_decoder (struct tar_stat_info *st, const char *keyword,
                                          const char *arg, size_t size);
static gboolean sparse_offset_decoder (struct tar_stat_info *st, const char *keyword,
                                       const char *arg, size_t size);
static gboolean sparse_numbytes_decoder (struct tar_stat_info *st, const char *keyword,
                                         const char *arg, size_t size);
static gboolean sparse_map_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                                    size_t size);
static gboolean dumpdir_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                                 size_t size);

/*** file scope variables ************************************************************************/

enum
{
    BILLION = 1000000000,
    LOG10_BILLION = 9
};

static struct xhdr_tab xhdr_tab[] = {
    { "atime", atime_decoder, 0 },
    { "comment", dummy_decoder, 0 },
    { "charset", dummy_decoder, 0 },
    { "ctime", ctime_decoder, 0 },
    { "gid", gid_decoder, 0 },
#if 0
    { "gname",                gname_decoder,            0  },
#endif
    { "linkpath", linkpath_decoder, 0 },
    { "mtime", mtime_decoder, 0 },
    { "path", path_decoder, 0 },
    { "size", size_decoder, 0 },
    { "uid", uid_decoder, 0 },
#if 0
    { "uname",                uname_decoder,            0  },
#endif

    // Sparse file handling
    { "GNU.sparse.name", sparse_path_decoder, XHDR_PROTECTED },
    { "GNU.sparse.major", sparse_major_decoder, XHDR_PROTECTED },
    { "GNU.sparse.minor", sparse_minor_decoder, XHDR_PROTECTED },
    { "GNU.sparse.realsize", sparse_size_decoder, XHDR_PROTECTED },
    { "GNU.sparse.numblocks", sparse_numblocks_decoder, XHDR_PROTECTED },

    { "GNU.sparse.size", sparse_size_decoder, XHDR_PROTECTED },
    /* tar 1.14 - 1.15.1 keywords. Multiple instances of these appeared in 'x'
       headers, and each of them was meaningful. It confilcted with POSIX specs,
       which requires that "when extended header records conflict, the last one
       given in the header shall take precedence." */
    { "GNU.sparse.offset", sparse_offset_decoder, XHDR_PROTECTED },
    { "GNU.sparse.numbytes", sparse_numbytes_decoder, XHDR_PROTECTED },
    // tar 1.15.90 keyword, introduced to remove the above-mentioned conflict.
    { "GNU.sparse.map", sparse_map_decoder, 0 },

    { "GNU.dumpdir", dumpdir_decoder, XHDR_PROTECTED },

    { NULL, NULL, 0 },
};

/* List of keyword/value pairs decoded from the last 'g' type header */
static GSList *global_header_override_list = NULL;

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */

static struct xhdr_tab *
locate_handler (const char *keyword)
{
    struct xhdr_tab *p;

    for (p = xhdr_tab; p->keyword != NULL; p++)
        if (strcmp (p->keyword, keyword) == 0)
            return p;

    return NULL;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
keyword_item_run (gpointer data, gpointer user_data)
{
    struct keyword_item *kp = (struct keyword_item *) data;
    struct tar_stat_info *st = (struct tar_stat_info *) user_data;
    struct xhdr_tab const *t;

    t = locate_handler (kp->pattern);
    if (t != NULL)
        return t->decoder (st, t->keyword, kp->value, strlen (kp->value));

    return TRUE;  // FIXME
}

/* --------------------------------------------------------------------------------------------- */

static void
keyword_item_free (gpointer data)
{
    struct keyword_item *kp = (struct keyword_item *) data;

    g_free (kp->pattern);
    g_free (kp->value);
    g_free (kp);
}

/* --------------------------------------------------------------------------------------------- */

static void
xheader_list_append (GSList **root, const char *kw, const char *value)
{
    struct keyword_item *kp;

    kp = g_new (struct keyword_item, 1);
    kp->pattern = g_strdup (kw);
    kp->value = g_strdup (value);
    *root = g_slist_prepend (*root, kp);
}

/* --------------------------------------------------------------------------------------------- */

static inline void
xheader_list_destroy (GSList **root)
{
    g_slist_free_full (*root, keyword_item_free);
    *root = NULL;
}

/* --------------------------------------------------------------------------------------------- */

static inline void
run_override_list (GSList *kp, struct tar_stat_info *st)
{
    g_slist_foreach (kp, (GFunc) keyword_item_run, st);
}

/* --------------------------------------------------------------------------------------------- */

static struct timespec
decode_timespec (const char *arg, char **arg_lim, gboolean parse_fraction)
{
    int ns = -1;
    gboolean overflow = FALSE;
    time_t s;
    char const *p;
    struct timespec r;

    s = stoint (arg, arg_lim, &overflow, TYPE_MINIMUM (time_t), TYPE_MAXIMUM (time_t));
    p = *arg_lim;

    if (p != arg)
    {
        ns = 0;

        if (parse_fraction && *p == '.')
        {
            int digits = 0;
            gboolean trailing_nonzero = FALSE;

            while (isdigit (*++p))
                if (digits < LOG10_BILLION)
                {
                    digits++;
                    ns = 10 * ns + (*p - '0');
                }
                else if (*p != '0')
                    trailing_nonzero = TRUE;

            *arg_lim = (char *) p;

            while (digits < LOG10_BILLION)
            {
                digits++;
                ns *= 10;
            }

            if (*arg == '-')
            {
                /* Convert "-1.10000000000001" to s == -2, ns == 89999999.
                   I.e., truncate time stamps towards minus infinity while
                   converting them to internal form.  */
                if (trailing_nonzero)
                    ns++;
                if (ns != 0)
                    ns = ckd_sub (&s, s, 1) ? -1 : BILLION - ns;
            }
        }

        if (overflow)
            ns = -1;
    }

    r.tv_sec = s;
    r.tv_nsec = ns;
    return r;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
decode_time (struct timespec *ts, const char *arg, const char *keyword)
{
    char *arg_lim = NULL;
    struct timespec t;

    (void) keyword;

    t = decode_timespec (arg, &arg_lim, TRUE);

    if (t.tv_nsec < 0)
        // Malformed extended header
        return FALSE;

    if (*arg_lim != '\0')
        // Malformed extended header
        return FALSE;

    *ts = t;

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
decode_signed_num (intmax_t *num, const char *arg, intmax_t minval, uintmax_t maxval,
                   const char *keyword)
{
    char *arg_lim = NULL;
    gboolean overflow = FALSE;
    intmax_t u;

    (void) keyword;

    u = stoint (arg, &arg_lim, &overflow, minval, maxval);

    if (arg_lim == arg || *arg_lim != '\0')
        return FALSE;  // malformed extended header

    if (overflow)
        return FALSE;  // out of range

    *num = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
decode_num (uintmax_t *num, const char *arg, uintmax_t maxval, const char *keyword)
{
    intmax_t i;

    if (!decode_signed_num (&i, arg, 0, maxval, keyword))
        return FALSE;

    *num = i;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
raw_path_decoder (struct tar_stat_info *st, const char *arg)
{
    if (*arg != '\0')
    {
        tar_assign_string_dup (&st->orig_file_name, arg);
        tar_assign_string_dup (&st->file_name, arg);
    }

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
dummy_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) st;
    (void) keyword;
    (void) arg;
    (void) size;

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
atime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    struct timespec ts;

    (void) size;

    if (!decode_time (&ts, arg, keyword))
        return FALSE;

    st->atime = ts;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
gid_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    intmax_t u;

    (void) size;

    if (!decode_signed_num (&u, arg, TYPE_MINIMUM (gid_t), TYPE_MINIMUM (gid_t), keyword))
        return FALSE;

    st->stat.st_gid = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

#if 0
static gboolean
gname_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;
    (void) size;

    tar_assign_string_dup (&st->gname, arg);
    return TRUE;
}
#endif

/* --------------------------------------------------------------------------------------------- */

static gboolean
linkpath_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;
    (void) size;

    tar_assign_string_dup (&st->link_name, arg);
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
ctime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    struct timespec ts;

    (void) size;

    if (!decode_time (&ts, arg, keyword))
        return FALSE;

    st->ctime = ts;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
mtime_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    struct timespec ts;

    (void) size;

    if (!decode_time (&ts, arg, keyword))
        return FALSE;

    st->mtime = ts;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
path_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;
    (void) size;

    if (!st->sparse_name_done)
        return raw_path_decoder (st, arg);

    return TRUE;  // FIXME
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
size_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    uintmax_t u;

    (void) size;

    if (!decode_num (&u, arg, TYPE_MAXIMUM (off_t), keyword))
        return FALSE;

    st->stat.st_size = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
uid_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    intmax_t u;

    (void) size;

    if (!decode_signed_num (&u, arg, TYPE_MINIMUM (uid_t), TYPE_MAXIMUM (uid_t), keyword))
        return FALSE;

    st->stat.st_uid = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

#if 0
static gboolean
uname_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;
    (void) size;

    tar_assign_string_dup (&st->uname, arg);
    return TRUE;
}
#endif

/* --------------------------------------------------------------------------------------------- */

static gboolean
dumpdir_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;

#if GLIB_CHECK_VERSION(2, 68, 0)
    st->dumpdir = g_memdup2 (arg, size);
#else
    st->dumpdir = g_memdup (arg, size);
#endif
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/**
 * Decodes a single extended header record, advancing @ptr to the next record.
 *
 * @param p pointer to extended header record
 * @param st stat info
 *
 * @return decode_record_ok or decode_record_finish on success, decode_record_fail otherwize
 */
static enum decode_record_status
decode_record (struct xheader *xhdr, char **ptr,
               gboolean (*handler) (void *data, const char *keyword, const char *value,
                                    size_t size),
               void *data)
{
    char *start = *ptr;
    char *p = start;
    idx_t len;
    char *len_lim = NULL;
    const char *keyword;
    char *nextp;
    idx_t len_max;
    gboolean ret;

    len_max = xhdr->buffer + xhdr->size - start;

    while (*p == ' ' || *p == '\t')
        p++;

    len = stoint (p, &len_lim, NULL, 0, IDX_MAX);

    if (len_lim == p)
        // Is the length missing?
        return (*p != '\0' ? decode_record_fail : decode_record_finish);

    if (len_max < len)
        return decode_record_fail;

    nextp = start + len;

    for (p = len_lim; *p == ' ' || *p == '\t'; p++)
        ;

    if (p == len_lim)
        return decode_record_fail;

    keyword = p;
    p = strchr (p, '=');
    if (!(p != NULL && p < nextp))
        return decode_record_fail;

    if (nextp[-1] != '\n')
        return decode_record_fail;

    *p = nextp[-1] = '\0';
    ret = handler (data, keyword, p + 1, nextp - p - 2);  // '=' + trailing '\n'
    *p = '=';
    nextp[-1] = '\n';
    *ptr = nextp;

    return (ret ? decode_record_ok : decode_record_fail);
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
decg (void *data, const char *keyword, const char *value, size_t size)
{
    GSList **kwl = (GSList **) data;
    struct xhdr_tab const *tab;

    (void) size;

    tab = locate_handler (keyword);
    if (tab != NULL && (tab->flags & XHDR_GLOBAL) != 0)
    {
        if (!tab->decoder (data, keyword, value, size))
            return FALSE;
    }
    else
        xheader_list_append (kwl, keyword, value);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
decx (void *data, const char *keyword, const char *value, size_t size)
{
    struct keyword_item kp = {
        .pattern = (char *) keyword,
        .value = (char *) value,
    };

    (void) size;

    return keyword_item_run (&kp, data);
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_path_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    (void) keyword;
    (void) size;

    st->sparse_name_done = TRUE;
    return raw_path_decoder (st, arg);
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_major_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    uintmax_t u;

    (void) size;

    if (!decode_num (&u, arg, INTMAX_MAX, keyword))
        return FALSE;

    st->sparse_major = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_minor_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    uintmax_t u;

    (void) size;

    if (!decode_num (&u, arg, INTMAX_MAX, keyword))
        return FALSE;

    st->sparse_minor = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_size_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    uintmax_t u;

    (void) size;

    if (!decode_num (&u, arg, TYPE_MAXIMUM (off_t), keyword))
        return FALSE;

    st->real_size_set = TRUE;
    st->real_size = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_numblocks_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                          size_t size)
{
    uintmax_t u;

    (void) size;

    if (!decode_num (&u, arg, SIZE_MAX, keyword))
        return FALSE;

    if (st->sparse_map == NULL)
        st->sparse_map = g_array_sized_new (FALSE, FALSE, sizeof (struct sp_array), u);
    else
        g_array_set_size (st->sparse_map, u);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_offset_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    uintmax_t u;
    struct sp_array *s;

    (void) size;

    if (!decode_num (&u, arg, TYPE_MAXIMUM (off_t), keyword))
        return FALSE;

    s = &g_array_index (st->sparse_map, struct sp_array, st->sparse_map->len - 1);
    s->offset = u;
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_numbytes_decoder (struct tar_stat_info *st, const char *keyword, const char *arg,
                         size_t size)
{
    uintmax_t u;
    struct sp_array s;

    (void) size;

    if (!decode_num (&u, arg, SIZE_MAX, keyword))
        return FALSE;

    s.offset = 0;
    s.numbytes = u;
    g_array_append_val (st->sparse_map, s);
    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
sparse_map_decoder (struct tar_stat_info *st, const char *keyword, const char *arg, size_t size)
{
    gboolean offset = TRUE;
    struct sp_array e;

    (void) keyword;
    (void) size;

    if (st->sparse_map != NULL)
        g_array_set_size (st->sparse_map, 0);

    while (TRUE)
    {
        off_t u;
        char *delim = NULL;
        gboolean overflow = FALSE;

        u = stoint (arg, &delim, &overflow, 0, TYPE_MAXIMUM (off_t));

        if (delim == arg)
        {
            // malformed extended header
            return FALSE;
        }

        if (offset)
        {
            e.offset = u;
            if (overflow)
            {
                // out of range
                return FALSE;
            }
        }
        else
        {
            e.numbytes = u;
            if (overflow)
            {
                // out of range
                return FALSE;
            }

            g_array_append_val (st->sparse_map, e);
        }

        offset = !offset;

        if (*delim == '\0')
            break;
        if (*delim != ',')
        {
            // malformed extended header
            return FALSE;
        }

        arg = delim + 1;
    }

    if (!offset)
    {
        // malformed extended header
        return FALSE;
    }

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */

/**
 * Decodes an extended header.
 *
 * @param st stat info
 *
 * @return TRUE on success, FALSE otherwize
 */
gboolean
tar_xheader_decode (struct tar_stat_info *st)
{
    char *p;
    enum decode_record_status status;

    run_override_list (global_header_override_list, st);

    p = st->xhdr.buffer + BLOCKSIZE;

    while ((status = decode_record (&st->xhdr, &p, decx, st)) == decode_record_ok)
        ;

    if (status == decode_record_fail)
        return FALSE;

    /* The archived (effective) file size is always set directly in tar header
       field, possibly overridden by "size" extended header - in both cases,
       result is now decoded in st->stat.st_size */
    st->archive_file_size = st->stat.st_size;

    /* The real file size (given by stat()) may be redefined for sparse
       files in "GNU.sparse.realsize" extended header */
    if (st->real_size_set)
        st->stat.st_size = st->real_size;

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

gboolean
tar_xheader_read (tar_super_t *archive, struct xheader *xhdr, union block *p, off_t size)
{
    size_t j = 0;
    size_t size_plus_1;

    size = MAX (0, size);
    if (ckd_add (&size_plus_1, size, BLOCKSIZE + 1))
        return FALSE;
    size = size_plus_1 - 1;
    xhdr->size = size;
    xhdr->buffer = g_malloc (size_plus_1);
    xhdr->buffer[size] = '\0';

    do
    {
        size_t len;

        if (p == NULL)
            return FALSE;  // Unexpected EOF in archive

        len = MIN (size, BLOCKSIZE);

        memcpy (xhdr->buffer + j, p->buffer, len);
        tar_set_next_block_after (p);
        p = tar_find_next_block (archive);

        j += len;
        size -= len;
    }
    while (size > 0);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

gboolean
tar_xheader_decode_global (struct xheader *xhdr)
{
    char *p;
    gboolean ret;

    p = xhdr->buffer + BLOCKSIZE;

    xheader_list_destroy (&global_header_override_list);

    while ((ret = decode_record (xhdr, &p, decg, &global_header_override_list)) == decode_record_ok)
        ;

    return (ret == decode_record_finish);
}

/* --------------------------------------------------------------------------------------------- */

void
tar_xheader_destroy (struct xheader *xhdr)
{
    MC_PTR_FREE (xhdr->buffer);
    xhdr->size = 0;
}

/* --------------------------------------------------------------------------------------------- */
