view tools/packed.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents e3f0eaf23f4f
children 9360a693d8af
line wrap: on
line source

/*
 * PACKed - PACKfile EDitor
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2011, 2018 Tecnic Software productions (TNSP)
 */
#include "dmtool.h"
#include "dmlib.h"
#include "dmargs.h"
#include "dmpack.h"
#include "dmfile.h"
#include "dmres.h"
#include <zlib.h>


#define	SET_MAX_FILES	  (4096)
#define	SET_TMPBUF_SIZE   (1024 * 1024)

enum
{
    CMD_NONE = 0,
    CMD_CREATE,
    CMD_ADD,
    CMD_LIST,
    CMD_EXTRACT
} DCOMMAND;

enum
{
    PACK_EXTRACTED = 0x0001,
};

int    nsrcFilenames = 0, nexcFilenames = 0;
char * srcFilenames[SET_MAX_FILES];
char * excFilenames[SET_MAX_FILES];

char * optPackFilename = NULL;
BOOL   optDoCompress = TRUE;
int    optCompressLevel = Z_BEST_COMPRESSION;
int    optCommand = CMD_NONE;
int    optDefResFlags = 0;


static const DMOptArg cmdList[] =
{
    { CMD_CREATE  , 'c', "create",      "Create and add files to PACK", OPT_NONE },
    { CMD_ADD     , 'a', "add",         "Add files to PACK", OPT_NONE },
    { CMD_LIST    , 'l', "list",        "List files in PACK", OPT_NONE },
    { CMD_EXTRACT , 'e', "extract",     "Extract files from PACK", OPT_NONE },
};

static const int cmdListN = sizeof(cmdList) / sizeof(cmdList[0]);


static const DMOptArg optList[] =
{
    {  0, '?', "help",        "Show this help", OPT_NONE },
    {  1, 'n', "nocompress",  "No compression / decompression", OPT_NONE },
    {  2, 'C', "level",       "Set zlib compression level 1-9", OPT_ARGREQ },
    {  3, 'v', "verbose",     "Increase verbosity", OPT_NONE },
    {  4, 'f', "resflags",    "Set default resource flags (-f 0xff)", OPT_ARGREQ },
    {  5, 'x', "exclude",     "Exclude name/glob pattern", OPT_ARGREQ },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "<command> [options] <packfile> [<filename(s)>]");

    for (int i = 0; i < cmdListN; i++)
    {
        const DMOptArg *cmd = &cmdList[i];
        char tmpStr[128];
        snprintf(tmpStr, sizeof(tmpStr), "%c / %s",
            cmd->o_short, cmd->o_long);

        fprintf(stdout, "  %-15s   %s\n", tmpStr, cmd->desc);
    }

    fprintf(stdout, "\n");

    dmArgsPrintHelp(stdout, optList, optListN, 0);

    fprintf(stdout,
        "\n"
        "Examples:\n"
        "$ %s l test.pak             -- list files in test.pak\n"
        "$ %s c test.pak foobar.jpg  -- create test.pak and add foobar.jpg in it\n"
        "$ %s e test.pak foobar.jpg  -- extract foobar.jpg from test.pak\n",
        dmProgName, dmProgName, dmProgName);
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    (void) currArg;
    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            optDoCompress = FALSE;
            break;

        case 2:
            optCompressLevel = atoi(optArg);
            if (optCompressLevel < 1 || optCompressLevel > 9)
            {
                dmErrorMsg("Invalid compression level argument '%s', must be 1 .. 9.\n", optArg);
                return FALSE;
            }
            break;

        case 3:
            dmVerbosity++;
            break;

        case 4:
            {
                unsigned int tmpUI;
                if (!dmGetIntVal(optArg, &tmpUI, NULL))
                {
                    dmErrorMsg("Invalid flags value '%s'.\n", optArg);
                    return FALSE;
                }
                optDefResFlags = tmpUI;
            }
            break;

        case 5:
            if (nexcFilenames < SET_MAX_FILES)
            {
                excFilenames[nexcFilenames++] = optArg;
            }
            else
            {
                dmErrorMsg("Maximum number of exclusion patterns (%d) exceeded!\n",
                      SET_MAX_FILES);
                return FALSE;
            }
            break;

        default:
            dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


BOOL argHandleNonOpt(char *currArg)
{
    if (optCommand == CMD_NONE)
    {
        for (int n = 0; n < cmdListN; n++)
        {
            const DMOptArg *cmd = &cmdList[n];
            if ((currArg[0] == cmd->o_short && currArg[1] == 0) ||
                strcmp(currArg, cmd->o_long) == 0)
            {
                optCommand = cmd->id;
                break;
            }
        }
        if (optCommand == CMD_NONE)
        {
            dmErrorMsg("Invalid command '%s'.\n", currArg);
            return FALSE;
        }
    }
    else
    if (optPackFilename == NULL)
    {
        optPackFilename = currArg;
    }
    else
    if (nsrcFilenames < SET_MAX_FILES)
    {
        srcFilenames[nsrcFilenames++] = currArg;
    }
    else
    {
        dmErrorMsg("Maximum number of input files (%d) exceeded!\n",
              SET_MAX_FILES);
        return FALSE;
    }
    return TRUE;
}


DMPackEntry *dmPackEntryCopy(const DMPackEntry *src)
{
    DMPackEntry *node = dmPackEntryNew();
    if (node == NULL)
        return NULL;

    strncpy(node->filename, src->filename, DMRES_NAME_LEN);
    node->filename[DMRES_NAME_LEN] = 0;

    node->size     = src->size;
    node->offset   = src->offset;
    node->length   = src->length;
    node->flags    = src->flags;

    return node;
}


int dmPackWrite(DMPackFile * pack)
{
    DMPackEntry *node;
    DMPackFileHeader hdr;

    if (pack == NULL)
        return DMERR_OK;

    if (pack->file == NULL)
        return DMERR_FOPEN;

    // Compute directory offset and number of entries
    memcpy(&hdr.ident, DPACK_IDENT, sizeof(hdr.ident));
    hdr.version = DPACK_VERSION;
    hdr.dirEntries = 0;
    hdr.dirOffset =
        sizeof(hdr.ident) + sizeof(hdr.version) +
        sizeof(hdr.dirEntries) + sizeof(hdr.dirOffset);

    for (node = pack->entries; node != NULL; node = node->next)
    {
        hdr.dirEntries++;
        hdr.dirOffset += node->length;
    }

    dmMsg(1, "%d entries in PACK, dir at offset 0x%08x.\n",
        hdr.dirEntries, hdr.dirOffset);

    // Write PACK header
    if (fseeko(pack->file, 0, SEEK_SET) != 0)
        return DMERR_FSEEK;

    if (!dm_fwrite_str(pack->file, (Uint8 *) & hdr.ident, sizeof(hdr.ident)) ||
        !dm_fwrite_le16(pack->file, hdr.version) ||
        !dm_fwrite_le32(pack->file, hdr.dirEntries) ||
        !dm_fwrite_le64(pack->file, hdr.dirOffset))
        return DMERR_FWRITE;

    // Write the directory
    if (fseeko(pack->file, hdr.dirOffset, SEEK_SET) != 0)
        return DMERR_FSEEK;

    for (node = pack->entries; node != NULL; node = node->next)
    {
        // Write one entry
        if (!dm_fwrite_str(pack->file, node->filename, DMRES_NAME_LEN) ||
            !dm_fwrite_le64(pack->file, node->offset) ||
            !dm_fwrite_le32(pack->file, node->length) ||
            !dm_fwrite_le32(pack->file, node->size) ||
            !dm_fwrite_le32(pack->file, node->flags))
            return DMERR_FWRITE;
    }

    return DMERR_OK;
}


int dmPackCreate(const char *filename, DMPackFile ** pack)
{
    // Allocate packfile-structure
    *pack = (DMPackFile *) dmMalloc0(sizeof(DMPackFile));
    if (*pack == NULL)
        return DMERR_MALLOC;

    // Open the file
    (*pack)->file = fopen(filename, "wb");
    if ((*pack)->file == NULL)
    {
        dmFree(*pack);
        return DMERR_FOPEN;
    }

    (*pack)->filename = dm_strdup(filename);

    // Set the result
    return DMERR_OK;
}


int dmPackAddFile(DMPackFile * pack, const char *filename,
    const Uint32 flags, const BOOL compress, int level,
    DMPackEntry ** ppEntry)
{
    Uint64 startOffs, outSize;
    Uint8 *inBuffer = NULL, *outBuffer = NULL;
    DMPackEntry entry, *node;
    FILE *inFile = NULL;
    int ret = DMERR_OK;
    BOOL zinit = FALSE;
    z_stream zstr;

    *ppEntry = NULL;

    if (pack == NULL)
        return DMERR_NULLPTR;

    if (pack->file == NULL)
        return DMERR_FOPEN;

    // Compute starting offset
    outSize = 0;
    startOffs = sizeof(DMPackFileHeader);
    for (node = pack->entries; node != NULL; node = node->next)
    {
        startOffs += node->length;
    }

    // Seek to the position
    if (fseeko(pack->file, startOffs, SEEK_SET) != 0)
        return DMERR_INVALID;

    // Read file data
    if ((inFile = fopen(filename, "rb")) == NULL)
        return DMERR_FOPEN;

    // Allocate temporary buffer
    if ((inBuffer = (Uint8 *) dmMalloc(SET_TMPBUF_SIZE)) == NULL ||
        (outBuffer = (Uint8 *) dmMalloc(SET_TMPBUF_SIZE)) == NULL)
    {
        ret = DMERR_MALLOC;
        goto out;
    }

    // Read (and possibly compress) the data
    if (compress)
    {
        int zret;
        dmMemset(&zstr, 0, sizeof(zstr));
        if (deflateInit(&zstr, level) != Z_OK)
        {
            ret = DMERR_COMPRESSION;
            goto out;
        }
        zinit = TRUE;

        // Initialize compression streams
        zret = Z_OK;
        while (!feof(inFile) && zret == Z_OK)
        {
            zstr.avail_in = fread(inBuffer, sizeof(Uint8), SET_TMPBUF_SIZE, inFile);

            zstr.next_in = inBuffer;
            zstr.next_out = outBuffer;
            zstr.avail_out = SET_TMPBUF_SIZE;
            zstr.total_out = 0;
            zret = deflate(&zstr, Z_FULL_FLUSH);

            if (zret == Z_OK && zstr.total_out > 0)
            {
                outSize += zstr.total_out;
                if (fwrite(outBuffer, sizeof(Uint8), zstr.total_out, pack->file) != zstr.total_out)
                {
                    ret = DMERR_FWRITE;
                    goto out;
                }
            }
        }
    }
    else
    {
        while (!feof(inFile))
        {
            size_t read = fread(inBuffer, sizeof(Uint8), SET_TMPBUF_SIZE, inFile);
            if (read > 0)
            {
                outSize += read;
                if (fwrite(inBuffer, sizeof(Uint8), read, pack->file) != read)
                {
                    ret = DMERR_FWRITE;
                    goto out;
                }
            }
        }
        zstr.total_in = outSize;
    }

    // Create directory entry
    strncpy(entry.filename, filename, DMRES_NAME_LEN);
    entry.filename[DMRES_NAME_LEN] = 0;
    entry.offset = startOffs;
    entry.size   = zstr.total_in;
    entry.length = outSize;
    entry.flags  = flags | (compress ? DMF_COMPRESSED : 0);

    // Add directory entry
    if ((*ppEntry = dmPackEntryCopy(&entry)) == NULL)
    {
        ret = DMERR_MALLOC;
        goto out;
    }

    dmPackEntryInsert(&pack->entries, *ppEntry);

out:
    // Cleanup
    if (zinit)
        deflateEnd(&zstr);

    dmFree(inBuffer);
    dmFree(outBuffer);
    if (inFile != NULL)
        fclose(inFile);

    return ret;
}


/*
 * EXTRACT a file from the PACK
 */
int dmPackExtractFile(DMPackFile *pack, DMPackEntry * entry, BOOL decompress)
{
    z_stream zstr;
    FILE *outFile = NULL;
    Uint8 *inBuffer = NULL, *outBuffer = NULL;
    size_t remaining;
    int zret, ret = DMERR_OK;
    BOOL zinit = FALSE;

    if (pack == NULL)
        return DMERR_NULLPTR;

    if (pack->file == NULL)
        return DMERR_FOPEN;

    // Seek to the position
    if (fseeko(pack->file, entry->offset, SEEK_SET) != 0)
        return DMERR_FSEEK;

    // Open destination file
    if ((outFile = fopen(entry->filename, "wb")) == NULL)
        return DMERR_FOPEN;

    // Allocate temporary buffer
    if ((inBuffer = (Uint8 *) dmMalloc(SET_TMPBUF_SIZE)) == NULL ||
        (outBuffer = (Uint8 *) dmMalloc(SET_TMPBUF_SIZE)) == NULL)
    {
        ret = DMERR_MALLOC;
        goto out;
    }

    // Read and uncompress the data, if needed
    if (decompress || (entry->flags & DMF_COMPRESSED) == 0)
    {
        dmMemset(&zstr, 0, sizeof(zstr));
        if (inflateInit(&zstr) != Z_OK)
        {
            ret = DMERR_COMPRESSION;
            goto out;
        }
        zinit = TRUE;
    }

    // Initialize compression streams
    remaining = entry->length;
    zret = Z_OK;
    while (remaining > 0 && zret == Z_OK)
    {
        size_t needed = remaining > SET_TMPBUF_SIZE ? SET_TMPBUF_SIZE : remaining;
        zstr.avail_in = fread(inBuffer, sizeof(Uint8), needed, pack->file);
        if (zstr.avail_in < needed)
        {
            ret = DMERR_FREAD;
            goto out;
        }

        remaining   -= zstr.avail_in;
        zstr.next_in = inBuffer;

        if (!decompress)
        {
            if (fwrite(inBuffer, sizeof(Uint8), zstr.avail_in, outFile) != zstr.avail_in)
            {
                ret = DMERR_FWRITE;
                goto out;
            }
        }
        else
        while (zstr.avail_in > 0 && zret == Z_OK)
        {
            zstr.next_out  = outBuffer;
            zstr.avail_out = SET_TMPBUF_SIZE;
            zstr.total_out = 0;
            zret = inflate(&zstr, Z_FULL_FLUSH);
            if (zstr.total_out > 0 &&
                fwrite(outBuffer, sizeof(Uint8), zstr.total_out, outFile) != zstr.total_out)
            {
                ret = DMERR_FWRITE;
                goto out;
            }
        }
    }

out:
    // Cleanup
    if (zinit)
        inflateEnd(&zstr);

    dmFree(inBuffer);
    dmFree(outBuffer);

    if (outFile != NULL)
        fclose(outFile);

    return ret;
}


/* Compare a string to a pattern. Case-SENSITIVE version.
 * The matching pattern can consist of any normal characters plus
 * wildcards ? and *. "?" matches any character and "*" matches
 * any number of characters.
 */
BOOL dmStrMatch(const char *str, const char *pattern)
{
    BOOL matched = TRUE, any = FALSE, end = FALSE;
    const char *tmp = NULL;

    // Check given pattern and string
    if (str == NULL || pattern == NULL)
        return FALSE;

    // Start comparision
    do {
        matched = FALSE;
        switch (*pattern)
        {
        case '?':
            // Any single character matches
            if (*str)
            {
                matched = TRUE;
                pattern++;
                str++;
            }
            break;

        case '*':
            matched = TRUE;
            pattern++;
            if (!*pattern)
                end = TRUE;
            any = TRUE;
            tmp = pattern;
            break;

        case 0:
            if (any)
            {
                if (*str)
                    str++;
                else
                    end = TRUE;
            }
            else
            {
                if (*str)
                {
                    if (tmp)
                    {
                        any = TRUE;
                        pattern = tmp;
                    }
                    else
                        matched = FALSE;
                }
                else
                    end = TRUE;
            }
            break;
        default:
            if (any)
            {
                if (*pattern == *str)
                {
                    any = FALSE;
                    matched = TRUE;
                }
                else
                {
                    if (*str)
                    {
                        matched = TRUE;
                        str++;
                    }
                }
            }
            else
            {
                if (*pattern == *str)
                {
                    matched = TRUE;
                    if (*pattern)
                        pattern++;
                    if (*str)
                        str++;
                }
                else
                {
                    if (tmp)
                    {
                        matched = TRUE;
                        any = TRUE;
                        pattern = tmp;
                    }
                }
            }

            if (!*str && !*pattern)
                end = TRUE;
            break;

        }        // switch

    } while (matched && !end);

    return matched;
}


BOOL dmCheckExcluded(const char *filename)
{
    for (int i = 0; i < nexcFilenames; i++)
    {
        if (dmStrMatch(filename, excFilenames[i]))
            return TRUE;
    }
    return FALSE;
}


int main(int argc, char *argv[])
{
    int res = 0;
    DMPackFile *pack = NULL;

    // Parse arguments
    dmInitProg("packed", "Pack File Editor", "0.7", NULL, NULL);
    dmVerbosity = 0;

    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleNonOpt, OPTH_BAILOUT))
        exit(1);

    if (optCommand == CMD_NONE)
    {
        dmErrorMsg("Nothing to do. Perhaps try \"%s --help\"?\n",
            dmProgName);
        goto error;
    }

    if (optPackFilename == NULL)
    {
        dmErrorMsg("No PACK file specified.\n");
        goto error;
    }

    dmMsg(1, "Processing PACK '%s' ...\n", optPackFilename);

    // Execute command
    switch (optCommand)
    {
    case CMD_CREATE:
    case CMD_ADD:
        switch (optCommand)
        {
        case CMD_CREATE:
            dmMsg(1, "Creating new PACK\n");
            res = dmPackCreate(optPackFilename, &pack);
            break;

        case CMD_ADD:
            dmMsg(1, "Opening existing PACK\n");
            res = dmPackOpen(optPackFilename, &pack, FALSE);
            break;
        }

        // Add files into PACK
        if (res == DMERR_OK)
        {
            dmMsg(1, "Adding files...\n");

            for (int i = 0; i < nsrcFilenames; i++)
            if (!dmCheckExcluded(srcFilenames[i]))
            {
                DMPackEntry *node = NULL;
                int res = dmPackAddFile(pack, srcFilenames[i],
                    optDefResFlags, optDoCompress,
                    optCompressLevel, &node);

                if (res != DMERR_OK)
                {
                    dmPrint(1, "%-32s [ERROR:%d]\n",
                        srcFilenames[i], res);
                }
                else
                {
                    dmPrint(1, "%-32s ['%s', s=%d, c=%d, o=%ld, f=0x%04x]\n",
                        srcFilenames[i], node->filename,
                        node->size, node->length, node->offset, node->flags);
                }
            }

            dmMsg(2, "w=%d\n", dmPackWrite(pack));
            dmMsg(2, "c=%d\n", dmPackClose(pack));
        }
        else
        {
            dmErrorMsg("Could not open packfile, error #%d: %s\n", res,
                  dmErrorStr(res));
        }
        break;

    case CMD_LIST:
        // List files in PACK
        res = dmPackOpen(optPackFilename, &pack, TRUE);
        if (res == DMERR_OK)
        {
            DMPackEntry *node;
            int i;
            for (i = 0, node = pack->entries; node; i++)
                node = node->next;

            dmMsg(1, "%d files total\n", i);

            dmPrint(0, "%-32s | %8s | %8s | %8s | %s\n",
                "Name", "Size", "CSize", "Offset", "ResFlags");

            for (node = pack->entries; node != NULL; node = node->next)
            {
                BOOL match;

                // Check for matches in specified filenames/patterns
                if (nsrcFilenames > 0)
                {
                    match = FALSE;
                    for (int i = 0; i < nsrcFilenames && !match; i++)
                        match = dmStrMatch(node->filename, srcFilenames[i]);
                }
                else
                // No filenames specified, everything shown
                    match = TRUE;

                if (match)
                {
                    dmPrint(0, "%-32s | %8d | %8d | %08x | %04x\n",
                        node->filename, node->size, node->length,
                        node->offset, node->flags);
                }
            }

            dmMsg(2, "c=%d\n", dmPackClose(pack));
        }
        else
            dmErrorMsg("Could not open packfile, error #%d: %s\n", res,
                  dmErrorStr(res));
        break;

    case CMD_EXTRACT:
        // Extract files from PACK
        res = dmPackOpen(optPackFilename, &pack, TRUE);
        if (res == DMERR_OK)
        {
            DMPackEntry *node;
            FILE *resFile = fopen(DMRES_RES_FILE, "w");
            if (resFile == NULL)
            {
                int err = dmGetErrno();
                dmErrorMsg("Could not create resource output file '%s' #%d: %s\n",
                    DMRES_RES_FILE, err, dmErrorStr(err));
            }

            for (node = pack->entries; node != NULL; node = node->next)
            {
                BOOL match;

                // Check for matches
                if (nsrcFilenames > 0)
                {
                    match = FALSE;
                    for (int i = 0; i < nsrcFilenames && !match; i++)
                    {
                        if (dmStrMatch(node->filename, srcFilenames[i]) &&
                            !dmCheckExcluded(node->filename))
                            match = TRUE;
                    }
                }
                else
                {
                    match = !dmCheckExcluded(node->filename);
                }

                if (match && (node->privFlags & PACK_EXTRACTED) == 0)
                {
                    // Mark as done
                    node->privFlags |= PACK_EXTRACTED;

                    // Print one entry
                    dmPrint(0, "Extracting: %-32s [siz=%d, cmp=%d, offs=0x%08x, flags=0x%04x]\n",
                            node->filename, node->size, node->length,
                            node->offset, node->flags);

                    dmPackExtractFile(pack, node, optDoCompress);
                }
            }

            dmMsg(2, "c=%d\n", dmPackClose(pack));

            if (resFile != NULL)
                fclose(resFile);
        }
        else
            dmErrorMsg("Could not open packfile, error #%d: %s\n", res,
                  dmErrorStr(res));
        break;
    }

error:
    return 0;
}