/*
 * Author:
 *      Guido Draheim <guidod@gmx.de>
 *      Tomi Ollila <too@iki.fi>
 *
 * Copyright (c) Guido Draheim, use under copyleft (LGPL,MPL)
 */

#include <zzip/lib.h> /* archive handling */
#include <zzip/file.h>
#include <zzip/format.h>
#include <zzip/fetch.h>

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#ifdef ZZIP_HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#include <zzip/__debug.h>
#include <zzip/__mmap.h>
#include <zzip/__param.h>

#define __sizeof(X) ((zzip_ssize_t) (sizeof(X)))

#ifndef ZZIP_EASY
/* per default, we use a little hack to correct bad z_rootseek parts */
#define ZZIP_CORRECT_ROOTSEEK 1
#endif

#if (__GNUC__ == 3 && __GNUC_MINOR__ >= 3) || (__GNUC__ >= 4)
#ifdef DEBUG
#warning suppress a warning where the compiler should have optimized instead.
#endif
#define _255 254
#else
#define _255 255
#endif

#define ZZIP_DISK64_TRAILER 1

#ifdef ZZIP_DISK64_TRAILER
struct _disk_trailer {
    void*        zz_tail;
    void*        zz_for_correct_rootseek; // ZZIP_CORRECT_ROOTSEEK
    zzip_off64_t zz_entries;
    zzip_off64_t zz_finalentries;
    zzip_off64_t zz_rootseek;
    zzip_off64_t zz_rootsize;
};
#define _disk_trailer_entries(__p)           ((__p)->zz_entries)
#define _disk_trailer_localentries(__p)      ((__p)->zz_entries)
#define _disk_trailer_finalentries(__p)      ((__p)->zz_finalentries)
#define _disk_trailer_rootseek(__p)          ((__p)->zz_rootseek)
#define _disk_trailer_rootsize(__p)          ((__p)->zz_rootsize)
#define _disk_trailer_set_rootseek(__p, __v) ((__p)->rootseek = (__v))

#else
#define _disk_trailer              zzip_disk_trailer
#define _disk_trailer_entries      zzip_disk_trailer_entries
#define _disk_trailer_localentries zzip_disk_trailer_localentries
#define _disk_trailer_finalentries zzip_disk_trailer_finalentries
#define _disk_trailer_rootseek     zzip_disk_trailer_rootseek
#define _disk_trailer_rootsize     zzip_disk_trailer_rootsize
#define _disk_trailer_set_rootseek zzip_disk_trailer_set_rootseek
#define __zzip_fetch_disk_trailer  __zzip_find_disk_trailer
#endif

/* ---------------------------  internals  -------------------------------- */

/* internal functions of zziplib, avoid at all cost, changes w/o warning.
 * we do export them for debugging purpose and special external tools
 * which know what they do and which can adapt from version to version
 */

static int
__zzip_fetch_disk_trailer(int fd, zzip_off_t filesize, struct _disk_trailer* _zzip_restrict trailer,
                          zzip_plugin_io_t io);
static int
__zzip_parse_root_directory(int fd, struct _disk_trailer* trailer, struct zzip_dir_hdr** hdr_return,
                            zzip_plugin_io_t io, zzip_off_t filesize);

_zzip_inline static char*
__zzip_aligned4(char* p);

/* ------------------------  harden routines ------------------------------ */

#ifdef ZZIP_HARDEN

/* internal.
 * check for inconsistent values in trailer and prefer lower seek value
 * - we fix values assuming the root directory was written at the end
 * and it is just before the zip trailer. Therefore, ...
 */
_zzip_inline static void
__fixup_rootseek(zzip_off_t offset_of_trailer, struct _disk_trailer* trailer)
{
    if (_disk_trailer_rootseek(trailer) > /* .. */
            offset_of_trailer - _disk_trailer_rootsize(trailer) &&
        offset_of_trailer > _disk_trailer_rootsize(trailer)) {
        register zzip_off_t offset;
        offset = offset_of_trailer - _disk_trailer_rootsize(trailer);
        _disk_trailer_set_rootseek(trailer, offset);
        HINT2("new rootseek=%li", (long) _disk_trailer_rootseek(trailer));
    }
}

#define __correct_rootseek(A, B, C)

#elif defined ZZIP_CORRECT_ROOTSEEK

/* store the seekvalue of the trailer into the "z_magic" field and with
 * a 64bit off_t we overwrite z_disk/z_finaldisk as well. If you change
 * anything in zziplib or dump the trailer structure then watch out that
 * these are still unused, so that this code may still (ab)use those. */
#define __fixup_rootseek(_offset_of_trailer, _trailer) *(zzip_off_t*) _trailer = _offset_of_trailer;
#define __correct_rootseek(_u_rootseek, _u_rootsize, _trailer)                                     \
    if (_u_rootseek > *(zzip_off_t*) _trailer - _u_rootsize)                                       \
        _u_rootseek = *(zzip_off_t*) _trailer - _u_rootsize;
#else
#define __fixup_rootseek(A, B)
#define __correct_rootseek(A, B, C)
#endif

#ifdef DEBUG
_zzip_inline static void
__debug_dir_hdr(struct zzip_dir_hdr* hdr)
{
    if (sizeof(struct zzip_dir_hdr) > sizeof(struct zzip_disk_entry)) {
        WARN1("internal sizeof-mismatch may break wreakage");
    }
    /*  the internal directory structure is never bigger than the
     *  external zip central directory space had been beforehand
     *  (as long as the following assertion holds...)
     */

    if (((zzip_off_t) hdr) & 3) {
        NOTE1("this machine's malloc(3) returns sth. not u32-aligned");
    }
    /* we assume that if this machine's malloc has returned a non-aligned
     * memory block, then it is actually safe to access misaligned data, and
     * since it does only affect the first hdr it should not even bring about
     * too much of that cpu's speed penalty
     */
}
#else
#define __debug_dir_hdr(X)
#endif

/* -------------------------- low-level interface -------------------------- */

#if defined BUFSIZ
#if BUFSIZ == 1024 || BUFSIZ == 512 || BUFSIZ == 256
#define ZZIP_BUFSIZ BUFSIZ
#endif
#endif

#ifndef ZZIP_BUFSIZ
#define ZZIP_BUFSIZ 512

/* #define ZZIP_BUFSIZ 64  / * for testing */
#endif

/** internal.
 * This function is used by => zzip_file_open. It tries to find
 * the zip's central directory info that is usually a few
 * bytes off the end of the file.
 */
static int
__zzip_fetch_disk_trailer(int fd, zzip_off_t filesize, struct _disk_trailer* _zzip_restrict trailer,
                          zzip_plugin_io_t io)
{
/* clang-format off */
#ifdef DEBUG
#define return(val) { e=val; HINT2("%s", zzip_strerror(e)); goto cleanup; }
#else
#define return(val) { e=val; goto cleanup; }
#endif
    /* clang-format on */

    register int e;

#ifndef _LOWSTK
    auto char buffer[2 * ZZIP_BUFSIZ];
    char*     buf = buffer;
#else
    char* buf = malloc(2 * ZZIP_BUFSIZ);
#endif
    zzip_off_t   offset = 0;
    zzip_ssize_t maplen = 0; /* mmap(),read(),getpagesize() use size_t !! */
    char*        fd_map = 0;

    if (! trailer) {
        return (EINVAL);
    }

    if (filesize < __sizeof(struct zzip_disk_trailer)) {
        return (ZZIP_DIR_TOO_SHORT);
    }

    if (! buf) {
        return (ZZIP_OUTOFMEM);
    }

    offset = filesize; /* a.k.a. old offset */
    while (1)          /* outer loop */
    {
        register unsigned char* mapped;

        if (offset <= 0) {
            return (ZZIP_DIR_EDH_MISSING);
        }

        /* trailer cannot be farther away than 64K from fileend */
        if (filesize - offset > 64 * 1024) {
            return (ZZIP_DIR_EDH_MISSING);
        }

        /* the new offset shall overlap with the area after the old offset! */
        if (USE_MMAP && io->fd.sys) {
            zzip_off_t mapoff = offset;
            {
                zzip_ssize_t pagesize = _zzip_getpagesize(io->fd.sys);
                if (pagesize < ZZIP_BUFSIZ)
                    goto non_mmap; /* an error? */
                if (mapoff == filesize && filesize > pagesize)
                    mapoff -= pagesize;
                if (mapoff < pagesize) {
                    maplen = (zzip_ssize_t) mapoff + pagesize;
                    mapoff = 0;
                }
                else {
                    mapoff -= pagesize;
                    maplen = 2 * pagesize;
                    if ((zzip_ssize_t) mapoff & (pagesize - 1)) { /*only 1. run */
                        pagesize -= (zzip_ssize_t) mapoff & (pagesize - 1);
                        mapoff += pagesize;
                        maplen -= pagesize;
                    }
                }
                if (mapoff + maplen > filesize)
                    maplen = filesize - mapoff;
            }

            fd_map = _zzip_mmap(io->fd.sys, fd, mapoff, (zzip_size_t) maplen);
            if (fd_map == MAP_FAILED)
                goto non_mmap;
            mapped = (unsigned char*) fd_map;
            offset = mapoff; /* success */
            HINT3("mapped *%p len=%li", fd_map, (long) maplen);
        }
        else {
        non_mmap:
            fd_map = 0; /* have no mmap */
            {
                zzip_off_t pagesize = ZZIP_BUFSIZ;
                if (offset == filesize && filesize > pagesize)
                    offset -= pagesize;
                if (offset < pagesize) {
                    maplen = (zzip_ssize_t) offset + pagesize;
                    offset = 0;
                }
                else {
                    offset -= pagesize;
                    maplen = 2 * pagesize;
                    if ((zzip_ssize_t) offset & (pagesize - 1)) { /*on 1st run */
                        pagesize -= (zzip_ssize_t) offset & (pagesize - 1);
                        offset += pagesize;
                        maplen -= pagesize;
                    }
                }
                if (offset + maplen > filesize)
                    maplen = filesize - offset;
            }

            if (io->fd.seeks(fd, offset, SEEK_SET) < 0) {
                return (ZZIP_DIR_SEEK);
            }
            if (io->fd.read(fd, buf, (zzip_size_t) maplen) < maplen) {
                return (ZZIP_DIR_READ);
            }
            mapped = (unsigned char*) buf;                      /* success */
            HINT5("offs=$%lx len=%li filesize=%li pagesize=%i", /* .. */
                  (long) offset, (long) maplen, (long) filesize, ZZIP_BUFSIZ);
        }

        /* if the file-comment is not present, it happens that the z_comment field
           often isn't either. The actual zip_disk_trailer is a bit shorter then,
           with __sizeof(((struct zzip_disk_trailer*)(0))->z_comment) == 2 */
#define __sizeof_z_comment 2
        { /* now, check for the trailer-magic, hopefully near the end of file */
            register unsigned char* end = mapped + maplen;
            register unsigned char* tail;
            for (tail = end - 1; (tail >= mapped); tail--) {
                if ((*tail == 'P') && /* quick pre-check for trailer magic */
                    end - tail >= __sizeof(struct zzip_disk_trailer) - __sizeof_z_comment &&
                    zzip_disk_trailer_check_magic(tail)) {
#ifndef ZZIP_DISK64_TRAILER
                    if (end - tail >= __sizeof(*trailer)) {
                        memcpy(trailer, tail, sizeof(*trailer));
                    }
                    else {
                        memcpy(trailer, tail, sizeof(*trailer) - sizeof_z_comment);
                        trailer->z_comment[0] = 0;
                        trailer->z_comment[1] = 0;
                    }
#else
                    struct zzip_disk_trailer* orig   = (struct zzip_disk_trailer*) tail;
                    trailer->zz_tail                 = tail;
                    trailer->zz_entries              = zzip_disk_trailer_localentries(orig);
                    trailer->zz_finalentries         = zzip_disk_trailer_finalentries(orig);
                    trailer->zz_rootseek             = zzip_disk_trailer_rootseek(orig);
                    trailer->zz_rootsize             = zzip_disk_trailer_rootsize(orig);
#endif
                    if (trailer->zz_rootseek < 0 || trailer->zz_rootsize < 0)
                        return (ZZIP_CORRUPTED); // forged value

                    __fixup_rootseek(offset + tail - mapped, trailer);
                    /*
                     * "extract data from files archived in a single zip file."
                     * So the file offsets must be within the current ZIP archive!
                     */
                    if (trailer->zz_rootseek >= filesize ||
                        (trailer->zz_rootseek + trailer->zz_rootsize) >= filesize)
                        return (ZZIP_CORRUPTED);
                    {
                        return (0);
                    }
                }
                else if ((*tail == 'P') && end - tail >= __sizeof(struct zzip_disk64_trailer) &&
                         zzip_disk64_trailer_check_magic(tail)) {
#ifndef ZZIP_DISK64_TRAILER
                    return (ZZIP_DIR_LARGEFILE);
#else
                    struct zzip_disk64_trailer* orig = (struct zzip_disk64_trailer*) tail;
                    trailer->zz_tail                 = tail;
                    trailer->zz_entries              = zzip_disk64_trailer_localentries(orig);
                    trailer->zz_finalentries         = zzip_disk64_trailer_finalentries(orig);
                    trailer->zz_rootseek             = zzip_disk64_trailer_rootseek(orig);
                    trailer->zz_rootsize             = zzip_disk64_trailer_rootsize(orig);
                    if (trailer->zz_rootseek < 0 || trailer->zz_rootsize < 0)
                        return (ZZIP_CORRUPTED); // forged value
                    /*
                     * "extract data from files archived in a single zip file."
                     * So the file offsets must be within the current ZIP archive!
                     */
                    if (trailer->zz_rootseek >= filesize ||
                        (trailer->zz_rootseek + trailer->zz_rootsize) >= filesize)
                        return (ZZIP_CORRUPTED);
                    {
                        return (0);
                    }
#endif
                }
            }
        }

        if (USE_MMAP && fd_map) {
            HINT3("unmap *%p len=%li", fd_map, (long) maplen);
            _zzip_munmap(io->fd.sys, fd_map, (zzip_size_t) maplen);
            fd_map = 0;
        }
    } /*outer loop */

cleanup:
    if (USE_MMAP && fd_map) {
        HINT3("unmap *%p len=%li", fd_map, (long) maplen);
        _zzip_munmap(io->fd.sys, fd_map, (zzip_size_t) maplen);
    }
#ifdef _LOWSTK
    free(buf);
#endif
#undef return
    return e;
}

/*
 * making pointer alignments to values that can be handled as structures
 * is tricky. We assume here that an align(4) is sufficient even for
 * 64 bit machines. Note that binary operations are not usually allowed
 * to pointer types but we do need only the lower bits in this implementation,
 * so we can just cast the value to a long value.
 */
_zzip_inline static char*
__zzip_aligned4(char* p)
{
#define aligned4 __zzip_aligned4
    p += ((intptr_t) p) & 1;
    p += ((intptr_t) p) & 2;
    return p;
}

/** internal.
 * This function is used by => zzip_file_open, it is usually called after
 * => __zzip_find_disk_trailer. It will parse the zip's central directory
 * information and create a zziplib private directory table in
 * memory.
 */
static int
__zzip_parse_root_directory(int fd, struct _disk_trailer* trailer, struct zzip_dir_hdr** hdr_return,
                            zzip_plugin_io_t io, zzip_off_t filesize)
{
    auto struct zzip_disk_entry dirent;
    struct zzip_dir_hdr*        hdr;
    struct zzip_dir_hdr*        hdr0;
    uint16_t*                   p_reclen = 0;
    zzip_off64_t                entries;
    zzip_off64_t                zz_offset; /* offset from start of root directory */
    char*                       fd_map      = 0;
    zzip_off64_t                zz_fd_gap   = 0;
    zzip_off64_t                zz_entries  = _disk_trailer_localentries(trailer);
    zzip_off64_t                zz_rootsize = _disk_trailer_rootsize(trailer);
    zzip_off64_t                zz_rootseek = _disk_trailer_rootseek(trailer);
    __correct_rootseek(zz_rootseek, zz_rootsize, trailer);

    if (zz_entries <= 0 || zz_rootsize < 0 || zz_rootseek < 0 || zz_rootseek >= filesize)
        return ZZIP_CORRUPTED;

    hdr0 = (struct zzip_dir_hdr*) malloc(zz_rootsize);
    if (! hdr0)
        return ZZIP_DIRSIZE;
    hdr = hdr0;
    __debug_dir_hdr(hdr);

    if (USE_MMAP && io->fd.sys) {
        zz_fd_gap = zz_rootseek & (_zzip_getpagesize(io->fd.sys) - 1);
        HINT4(" fd_gap=%ld, mapseek=0x%lx, maplen=%ld", (long) (zz_fd_gap),
              (long) (zz_rootseek - zz_fd_gap), (long) (zz_rootsize + zz_fd_gap));
        fd_map = _zzip_mmap(io->fd.sys, fd, zz_rootseek - zz_fd_gap, zz_rootsize + zz_fd_gap);
        /* if mmap failed we will fallback to seek/read mode */
        if (fd_map == MAP_FAILED) {
            NOTE2("map failed: %s", strerror(errno));
            fd_map = 0;
        }
        else {
            HINT3("mapped *%p len=%li", fd_map, (long) (zz_rootsize + zz_fd_gap));
        }
    }

    for (entries = 0, zz_offset = 0;; entries++) {
        register struct zzip_disk_entry* d;
        uint16_t                         u_extras, u_comment, u_namlen;

#ifndef ZZIP_ALLOW_MODULO_ENTRIES
        if (entries >= zz_entries) {
            if (zz_offset + 256 < zz_rootsize) {
                FAIL4("%li's entry is long before the end of directory - enable modulo_entries? "
                      "(O:%li R:%li)", /* .. */
                      (long) entries, (long) (zz_offset), (long) zz_rootsize);
            }
            break;
        }
#endif

        if (fd_map) {
            d = (void*) (fd_map + zz_fd_gap + zz_offset); /* fd_map+fd_gap==u_rootseek */
        }
        else {
            if (io->fd.seeks(fd, zz_rootseek + zz_offset, SEEK_SET) < 0) {
                free(hdr0);
                return ZZIP_DIR_SEEK;
            }
            if (io->fd.read(fd, &dirent, sizeof(dirent)) < __sizeof(dirent)) {
                free(hdr0);
                return ZZIP_DIR_READ;
            }
            d = &dirent;
        }

        if ((zzip_off64_t) (zz_offset + sizeof(*d)) > zz_rootsize || /* .. */
            (zzip_off64_t) (zz_offset + sizeof(*d)) < 0) {
            FAIL4("%li's entry stretches beyond root directory (O:%li R:%li)", (long) entries,
                  (long) (zz_offset), (long) zz_rootsize);
            break;
        }

        if (! zzip_disk_entry_check_magic(d)) {
#ifndef ZZIP_ALLOW_MODULO_ENTRIES
            FAIL4("%li's entry has no disk_entry magic indicator (O:%li R:%li)", /* .. */
                  (long) entries, (long) (zz_offset), (long) zz_rootsize);
#endif
            break;
        }

#if 0 && defined DEBUG
        zzip_debug_xbuf((unsigned char *) d, sizeof(*d) + 8);
#endif

        u_extras  = zzip_disk_entry_get_extras(d);
        u_comment = zzip_disk_entry_get_comment(d);
        u_namlen  = zzip_disk_entry_get_namlen(d);
        HINT5("offset=0x%lx, size %ld, dirent *%p, hdr %p\n", /* .. */
              (long) (zz_offset + zz_rootseek), (long) zz_rootsize, d, hdr);

        /* writes over the read buffer, Since the structure where data is
           copied is smaller than the data in buffer this can be done.
           It is important that the order of setting the fields is considered
           when filling the structure, so that some data is not trashed in
           first structure read.
           at the end the whole copied list of structures  is copied into
           newly allocated buffer */
        hdr->d_crc32 = zzip_disk_entry_get_crc32(d);
        hdr->d_csize = zzip_disk_entry_get_csize(d);
        hdr->d_usize = zzip_disk_entry_get_usize(d);
        hdr->d_off   = zzip_disk_entry_get_offset(d);
        hdr->d_compr = zzip_disk_entry_get_compr(d);
        if (hdr->d_compr > _255)
            hdr->d_compr = 255;

        if ((zzip_off64_t) (zz_offset + sizeof(*d) + u_namlen) > zz_rootsize ||
            (zzip_off64_t) (zz_offset + sizeof(*d) + u_namlen) < 0) {
            FAIL4("%li's name stretches beyond root directory (O:%li N:%li)", /* .. */
                  (long) entries, (long) (zz_offset), (long) (u_namlen));
            break;
        }

        if (fd_map) {
            memcpy(hdr->d_name, fd_map + zz_fd_gap + zz_offset + sizeof(*d), u_namlen);
        }
        else {
            io->fd.read(fd, hdr->d_name, u_namlen);
        }
        hdr->d_name[u_namlen] = '\0';
        hdr->d_namlen         = u_namlen;

        /* there are some zip files around that do not encode the diskstart field,
           so the actual zzip_extras_zip64 block is a bit shorter */
#define __sizeof_z_diskstart64 4
        /* looking for ZIP64 extras when csize on intmax */
        if (u_extras >= (sizeof(struct zzip_extra_zip64) - __sizeof_z_diskstart64) &&
            (hdr->d_csize & 0xFFFFu == 0xFFFFu)) {
            DBG3("%i extras bytes (%i)", u_extras, sizeof(struct zzip_extra_zip64));
            zzip_off64_t zz_extras = zz_offset + sizeof(*d) + u_namlen;
            zzip_byte_t* extras_ptr;
            if (fd_map) {
                zzip_byte_t* extras_ptr = fd_map + zz_fd_gap + zz_extras;
            }
            else {
                extras_ptr = malloc(u_extras);
                io->fd.read(fd, extras_ptr, u_extras);
            }
            if (ZZIP_EXTRA_ZIP64_CHECK(extras_ptr)) {
                struct zzip_extra_zip64* zip64 = (struct zzip_extra_zip64*) extras_ptr;
                WARN1("ZIP64 support incomplete");
                hdr->d_csize = zzip_extra_zip64_csize(zip64);
                hdr->d_usize = zzip_extra_zip64_usize(zip64);
                hdr->d_off   = zzip_extra_zip64_offset(zip64);
            }
            else {
                DBG2("unknown extras block %04x", ZZIP_GETEXTRA(extras_ptr));
            }
            if (! fd_map) {
                free(extras_ptr);
            }
        }

        /* update offset by the total length of this entry -> next entry */
        zz_offset += sizeof(*d) + u_namlen + u_extras + u_comment;

        if (zz_offset > zz_rootsize) {
            FAIL3("%li's entry stretches beyond root directory (O:%li)", /* .. */
                  (long) entries, (long) (zz_offset));
            entries++;
            break;
        }

        HINT5("file %ld { compr=%d crc32=$%x offset=%d", /* .. */
              (long) entries, hdr->d_compr, hdr->d_crc32, hdr->d_off);
        HINT5("csize=%d usize=%d namlen=%d extras=%d", /* .. */
              hdr->d_csize, hdr->d_usize, u_namlen, u_extras);
        HINT5("comment=%d name='%s' %s <sizeof %d> } ", /* .. */
              u_comment, hdr->d_name, "", (int) sizeof(*d));

        p_reclen = &hdr->d_reclen;

        {
            register char* p = (char*) hdr;
            register char* q = aligned4(p + sizeof(*hdr) + u_namlen + 1);
            *p_reclen        = (uint16_t) (q - p);
            hdr              = (struct zzip_dir_hdr*) q;
        }
    } /*for */

    if (USE_MMAP && fd_map) {
        HINT3("unmap *%p len=%li", fd_map, (long) (zz_rootsize + zz_fd_gap));
        _zzip_munmap(io->fd.sys, fd_map, zz_rootsize + zz_fd_gap);
    }

    if (p_reclen) {
        *p_reclen = 0; /* mark end of list */

        if (hdr_return)
            *hdr_return = hdr0;
        else {
            /* If it is not assigned to *hdr_return, it will never be free()'d */
            free(hdr0);
        }
    } /* else zero (sane) entries */
    else
        free(hdr0);
#ifndef ZZIP_ALLOW_MODULO_ENTRIES
    return (entries != zz_entries) ? ZZIP_CORRUPTED : 0;
#else
    return ((entries & (unsigned) 0xFFFF) != zz_entries) ? ZZIP_CORRUPTED : 0;
#endif
}

/* ------------------------- high-level interface ------------------------- */

#ifndef O_BINARY
#define O_BINARY 0
#endif

static zzip_strings_t*
zzip_get_default_ext(void)
{
    static zzip_strings_t ext[] = {".zip", ".ZIP", /* common extension */
#ifdef ZZIP_USE_ZIPLIKES
                                   ".pk3", ".PK3", /* ID Software's Quake3 zipfiles */
                                   ".jar", ".JAR", /* Java zipfiles */
#endif
                                   0};

    return ext;
}

/** start usage. also: zzip_dir_free
 * This function allocates a new ZZIP_DIR handle and do basic
 * initializations before usage by => zzip_dir_fdopen
 * => zzip_dir_open => zzip_file_open or through
 * => zzip_open
 * (ext==null flags uses { ".zip" , ".ZIP" } )
 * (io ==null flags use of posix io defaults)
 */
ZZIP_DIR*
zzip_dir_alloc_ext_io(zzip_strings_t* ext, const zzip_plugin_io_t io)
{
    ZZIP_DIR* dir;
    if ((dir = (ZZIP_DIR*) calloc(1, sizeof(*dir))) == NULL)
        return 0;

    /* dir->fileext is currently unused - so what, still initialize it */
    dir->fileext = ext ? ext : zzip_get_default_ext();
    dir->io      = io ? io : zzip_get_default_io();
    return dir;
}

/** => zzip_dir_alloc_ext_io
 * This function is obsolete - it was generally used for implementation
 * and exported to let other code build on it. It is now advised to
 * use => zzip_dir_alloc_ext_io now on explicitly, just set that second
 * argument to zero to achieve the same functionality as the old style.
 */
ZZIP_DIR*
zzip_dir_alloc(zzip_strings_t* fileext)
{
    return zzip_dir_alloc_ext_io(fileext, 0);
}

/** also: zzip_dir_alloc_ext_io
 * This function will free the zzip_dir handle unless there are still
 * zzip_files attached (that may use its cache buffer).
 * This is the inverse of => zzip_dir_alloc , and both
 * are helper functions used implicitly in other zzipcalls
 * e.g. => zzip_dir_close = zzip_close
 *
 * returns zero on success
 * returns the refcount when files are attached.
 */
int
zzip_dir_free(ZZIP_DIR* dir)
{
    if (dir->refcount)
        return (dir->refcount); /* still open files attached */

    if (dir->fd >= 0) {
        dir->io->fd.close(dir->fd);
        dir->fd = 0;
    }
    if (dir->hdr0) {
        free(dir->hdr0);
        dir->hdr0 = NULL;
    }
    if (dir->cache.fp) {
        free(dir->cache.fp);
        dir->cache.fp = NULL;
    }
    if (dir->cache.buf32k) {
        free(dir->cache.buf32k);
        dir->cache.buf32k = NULL;
    }
    if (dir->realname) {
        free(dir->realname);
        dir->realname = NULL;
    }
    free(dir);
    return 0;
}

/** => zzip_dir_free
 * It will also => free(2) the => ZZIP_DIR-handle given.
 * the counterpart for => zzip_dir_open
 * see also => zzip_dir_free
 */
int
zzip_dir_close(ZZIP_DIR* dir)
{
    dir->refcount &= ~0x10000000; /* explicit dir close */
    return zzip_dir_free(dir);
}

/** fd reopen.
 * This function is used by the => zzip_dir_open and zzip_opendir(2) call.
 * Opens the zip-archive as specified with the fd which points to an
 * already openend file. This function then search and parse
 * the zip's central directory.
 *
 * NOTE: refcount is zero, so an _open/_close pair will also delete
 *       this _dirhandle
 */
ZZIP_DIR*
zzip_dir_fdopen(int fd, zzip_error_t* errcode_p)
{
    return zzip_dir_fdopen_ext_io(fd, errcode_p, 0, 0);
}

static zzip_error_t
__zzip_dir_parse(ZZIP_DIR* dir); /* forward */

/** => zzip_dir_fdopen
 * this function uses explicit ext and io instead of the internal
 * defaults, setting these to zero is equivalent to => zzip_dir_fdopen
 */
ZZIP_DIR*
zzip_dir_fdopen_ext_io(int fd, zzip_error_t* errcode_p, zzip_strings_t* ext,
                       const zzip_plugin_io_t io)
{
    zzip_error_t rv;
    ZZIP_DIR*    dir;

    if ((dir = zzip_dir_alloc_ext_io(ext, io)) == NULL) {
        rv = ZZIP_OUTOFMEM;
        goto error;
    }

    dir->fd = fd;
    if ((rv = __zzip_dir_parse(dir)))
        goto error;

    dir->hdr = dir->hdr0;
    dir->refcount |= 0x10000000;

    if (errcode_p)
        *errcode_p = rv;
    return dir;
error:
    if (dir) {
        /*
         * in case of error, fd will be closed by caller,
         * don't take the fd ownership, otherwise fd will be closed twice
         */
        dir->fd = -1;
        zzip_dir_free(dir);
    }
    if (errcode_p)
        *errcode_p = rv;
    return NULL;
}

static zzip_error_t
__zzip_dir_parse(ZZIP_DIR* dir)
{
    zzip_error_t         rv;
    zzip_off_t           filesize;
    struct _disk_trailer trailer;
    /* if (! dir || dir->fd < 0)
     *     { rv = EINVAL; goto error; }
     */

    HINT2("------------------ fd=%i", (int) dir->fd);
    if ((filesize = dir->io->fd.filesize(dir->fd)) < 0) {
        rv = ZZIP_DIR_STAT;
        goto error;
    }

    HINT2("------------------ filesize=%ld", (long) filesize);
    if ((rv = __zzip_fetch_disk_trailer(dir->fd, filesize, &trailer, dir->io)) != 0) {
        goto error;
    }

    HINT5("directory = { entries= %ld/%ld, size= %ld, seek= %ld } ",
          (long) _disk_trailer_localentries(&trailer), (long) _disk_trailer_finalentries(&trailer),
          (long) _disk_trailer_rootsize(&trailer), (long) _disk_trailer_rootseek(&trailer));

    if ((rv = __zzip_parse_root_directory(dir->fd, &trailer, &dir->hdr0, dir->io, filesize)) != 0) {
        goto error;
    }
error:
    return rv;
}

/** internal.
 * This function will attach any of the .zip extensions then
 * trying to open it the with => open(2). This is a helper
 * function for => zzip_dir_open, => zzip_opendir and => zzip_open.
 *
 * This function returns a new system file handle or -1 on error.
 * On error this function leaves the errno(3) of the underlying
 * open(2) call on the last file.
 */
int
__zzip_try_open(zzip_char_t* filename, int filemode, zzip_strings_t* ext, zzip_plugin_io_t io)
{
    auto char   file[PATH_MAX];
    int         fd;
    zzip_size_t len = strlen(filename);

    if (len + 4 >= PATH_MAX) {
        errno = ENAMETOOLONG;
        return -1;
    }
    memcpy(file, filename, len + 1);

    if (! io)
        io = zzip_get_default_io();
    if (! ext)
        ext = zzip_get_default_ext();

    for (; *ext; ++ext) {
        strcpy(file + len, *ext);
        fd = (io->fd.open)(file, filemode);
        if (fd != -1)
            return fd;
    }
    return -1;
}

/** open zip-archive.
 * This function opens the given zip-archive (if available).
 * The two ext_io arguments will default to use posix io and
 * a set of default fileext that can atleast add .zip ext itself.
 */
ZZIP_DIR*
zzip_dir_open(zzip_char_t* filename, zzip_error_t* e)
{
    return zzip_dir_open_ext_io(filename, e, 0, 0);
}

/** => zzip_dir_open
 * this function uses explicit ext and io instead of the internal
 * defaults. Setting these to zero is equivalent to => zzip_dir_open
 * Note that the referenced ext_io plugin handlers structure must be
 * static as it is not copied to the returned ZZIP_DIR structure.
 */
ZZIP_DIR*
zzip_dir_open_ext_io(zzip_char_t* filename, zzip_error_t* e, zzip_strings_t* ext,
                     zzip_plugin_io_t io)
{
    int fd;

    if (! io)
        io = zzip_get_default_io();
    if (! ext)
        ext = zzip_get_default_ext();

    fd = (io->fd.open)(filename, O_RDONLY | O_BINARY);
    if (fd != -1) {
        return zzip_dir_fdopen_ext_io(fd, e, ext, io);
    }
    else {
        fd = __zzip_try_open(filename, O_RDONLY | O_BINARY, ext, io);
        if (fd != -1) {
            return zzip_dir_fdopen_ext_io(fd, e, ext, io);
        }
        else {
            if (e) {
                *e = ZZIP_DIR_OPEN;
            }
            return 0;
        }
    }
}

/** => zzip_dir_open
 * fills the dirent-argument with the values and
 * increments the read-pointer of the dir-argument.
 *
 * returns 0 if there no entry (anymore).
 */
int
zzip_dir_read(ZZIP_DIR* dir, ZZIP_DIRENT* d)
{
    if (! dir || ! dir->hdr || ! d)
        return 0;

    d->d_compr = dir->hdr->d_compr;
    d->d_csize = dir->hdr->d_csize;
    d->st_size = dir->hdr->d_usize;
    d->d_name  = dir->hdr->d_name;

    if (! dir->hdr->d_reclen) {
        dir->hdr = 0;
    }
    else {
        dir->hdr = (struct zzip_dir_hdr*) ((char*) dir->hdr + dir->hdr->d_reclen);
    }

    return 1;
}
