root/src/bin/xres.cpp
/*
 * Copyright 2005-2010, Ingo Weinhold, bonefish@users.sf.net.
 * Distributed under the terms of the MIT License.
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <string>

#include <List.h>
#include <Resources.h>
#include <StorageDefs.h>
#include <TypeConstants.h>


using namespace std;


static const char *kCommandName = "xres";
static const char *kDefaultResourceName = NULL;
static const char *kDefaultOutputFile = "xres.output.rsrc";
static const int kMaxSaneResourceSize = 100 * 1024 * 1024;      // 100 MB

static int kArgc;
static const char *const *kArgv;

// usage
const char *kUsage =
"Usage: %s ( -h | --help )\n"
"       %s -l <file> ...\n"
"       %s <command> ...\n"
"\n"
"The first form prints this help text and exits.\n"
"\n"
"The second form lists the resources of all given files.\n"
"\n"
"The third form manipulates the resources of one or more files according to\n"
"the given commands.\n"
"\n"
"Valid commands are:\n"
"  <input file>\n"
"         - Add the resources read from file <input file> to the current\n"
"           output file. The file can either be a resource file or an\n"
"           executable file.\n"
"  -a <type>:<id>[:<name>] ( <file> |  -s <data> )\n"
"         - Add a resource to the current output file. The added resource is\n"
"           of type <type> and has the ID <id>. If given the resource will\n"
"           have name <name>, otherwise it won't have a name. The resource\n"
"           data will either be the string <data> provided on the command\n"
"           line or the data read from file <file> (the whole contents).\n"
"  -d <type>[:<id>]\n"
"         - Excludes resources with type <type> and, if given, ID <id> from\n"
"           being written to the output file. This applies to all resources\n"
"           read from input files or directly specified via command \"-a\"\n"
"           following this command until the next \"-d\" command.\n"
"  -o <output file>\n"
"         - Changes the output file to <output file>. All resources specified\n"
"           by subsequent <input file> or \"-a\" commands will be written\n"
"           to this file until the next output file is specified via the\n"
"           \"-o\" command. Resources specified later overwrite earlier ones\n"
"           with the same type and ID. If <output file> doesn't exist yet, \n"
"           a resource file with the name will be created. If it exists and\n"
"           is an executable file, the resources will be added to it (if the\n"
"           file already has resources, they will be removed before). If it\n"
"           is a resource file or a file of unknown type, it will be\n"
"           overwritten with a resource file containing the specified\n"
"           resources. The initial output file is \"xres.output.rsrc\".\n"
"           Note that an output file will only be created or modified, if at\n"
"           least one <input file> or \"-a\" command is given for it.\n"
"  -x <type>[:<id>]\n"
"         - Only resources with type <type> and, if given, ID <id> will be\n"
"           written to the output file. This applies to all resources\n"
"           read from input files or directly specified via command \"-a\"\n"
"           following this command until the next \"-x\" command.\n"
"  --     - All following arguments, even if starting with a \"-\" character,\n"
"           are treated as input file names.\n"
"\n"
"Parameters:\n"
"  <type> - A type constant consisting of exactly four characters.\n"
"  <id>   - A positive or negative integer.\n"
;


// resource_type
static const char *
resource_type(type_code type)
{
        static char typeString[5];

        typeString[0] = type >> 24;
        typeString[1] = (type >> 16) & 0xff;
        typeString[2] = (type >> 8) & 0xff;
        typeString[3] = type & 0xff;
        typeString[4] = '\0';

        return typeString;
}


// ResourceID
struct ResourceID {
        type_code       type;
        int32           id;
        bool            wildcardID;

        ResourceID(type_code type = B_ANY_TYPE, int32 id = 0,
                        bool wildcardID = true)
                :
                type(type),
                id(id),
                wildcardID(wildcardID)
        {
        }

        ResourceID(const ResourceID &other)
        {
                *this = other;
        }

        bool Matches(const ResourceID &other) const
        {
                return ((type == other.type || type == B_ANY_TYPE)
                        && (wildcardID || id == other.id));
        }

        ResourceID &operator=(const ResourceID &other)
        {
                type = other.type;
                id = other.id;
                wildcardID = other.wildcardID;
                return *this;
        }
};


// ResourceDataSource
struct ResourceDataSource {
        ResourceDataSource()
        {
        }

        virtual ~ResourceDataSource()
        {
        }

        virtual void GetData(const void *&data, size_t &size) = 0;

        virtual void Flush()
        {
        }
};


// MemoryResourceDataSource
struct MemoryResourceDataSource : ResourceDataSource {
        MemoryResourceDataSource(const void *data, size_t size, bool clone)
        {
                _Init(data, size, clone);
        }

        MemoryResourceDataSource(const char *data, bool clone)
        {
                _Init(data, strlen(data) + 1, clone);
        }

        virtual ~MemoryResourceDataSource()
        {
                if (fOwner)
                        delete[] fData;
        }

        virtual void GetData(const void *&data, size_t &size)
        {
                data = fData;
                size = fSize;
        }

private:
        void _Init(const void *data, size_t size, bool clone)
        {
                if (clone) {
                        fData = new uint8[size];
                        memcpy(fData, data, size);
                        fSize = size;
                        fOwner = true;
                } else {
                        fData = (uint8*)data;
                        fSize = size;
                        fOwner = false;
                }
        }

private:
        uint8   *fData;
        size_t  fSize;
        bool    fOwner;
};


// FileResourceDataSource
struct FileResourceDataSource : ResourceDataSource {
        FileResourceDataSource(const char *path)
                :
                fPath(path),
                fData(NULL),
                fSize(0)
        {
        }

        virtual ~FileResourceDataSource()
        {
                Flush();
        }

        virtual void GetData(const void *&_data, size_t &_size)
        {
                if (!fData) {
                        // open the file for reading
                        BFile file;
                        status_t error = file.SetTo(fPath.c_str(), B_READ_ONLY);
                        if (error != B_OK) {
                                fprintf(stderr, "Error: Failed to open file \"%s\": %s\n",
                                        fPath.c_str(), strerror(error));

                                exit(1);
                        }

                        // get size
                        off_t size;
                        error = file.GetSize(&size);
                        if (error != B_OK) {
                                fprintf(stderr, "Error: Failed to get size of file \"%s\": "
                                        "%s\n", fPath.c_str(), strerror(error));

                                exit(1);
                        }

                        // check size
                        if (size > kMaxSaneResourceSize) {
                                fprintf(stderr, "Error: Resource data file \"%s\" is too big\n",
                                        fPath.c_str());

                                exit(1);
                        }

                        // read the data
                        fData = new uint8[size];
                        fSize = size;
                        ssize_t bytesRead = file.ReadAt(0, fData, fSize);
                        if (bytesRead < 0) {
                                fprintf(stderr, "Error: Failed to read data size from file "
                                        "\"%s\": %s\n", fPath.c_str(), strerror(bytesRead));

                                exit(1);
                        }
                }

                _data = fData;
                _size = fSize;
        }

        virtual void Flush()
        {
                if (fData) {
                        delete[] fData;
                        fData = NULL;
                }
        }

private:
        string  fPath;
        uint8   *fData;
        size_t  fSize;
};


// State
struct State {
        State()
        {
        }

        virtual ~State()
        {
        }

        virtual void SetOutput(const char *path)
        {
                (void)path;
        }

        virtual void ProcessInput(const char *path)
        {
                (void)path;
        }

        virtual void SetInclusionPattern(const ResourceID &pattern)
        {
                (void)pattern;
        }

        virtual void SetExclusionPattern(const ResourceID &pattern)
        {
                (void)pattern;
        }

        virtual void AddResource(const ResourceID &id, const char *name,
                ResourceDataSource *dataSource)
        {
                (void)id;
                (void)name;
                (void)dataSource;
        }
};


// ListState
struct ListState : State {
        ListState()
        {
        }

        virtual ~ListState()
        {
        }

        virtual void ProcessInput(const char *path)
        {
                // open the file for reading
                BFile file;
                status_t error = file.SetTo(path, B_READ_ONLY);
                if (error != B_OK) {
                        fprintf(stderr, "Error: Failed to open file \"%s\": %s\n", path,
                                strerror(error));
                        exit(1);
                }

                // open the resources
                BResources resources;
                error = resources.SetTo(&file, false);
                if (error != B_OK) {
                        if (error == B_ERROR) {
                                fprintf(stderr, "Error: File \"%s\" is not a resource file.\n",
                                path);
                        } else {
                                fprintf(stderr, "Error: Failed to read resources from file "
                                        "\"%s\": %s\n", path, strerror(error));
                        }

                        exit(1);
                }

                // print resources
                printf("\n%s resources:\n\n", path);
                printf(" type           ID        size  name\n");
                printf("------ ----------- -----------  --------------------\n");

                type_code type;
                int32 id;
                const char *name;
                size_t size;
                for (int32 i = 0;
                                resources.GetResourceInfo(i, &type, &id, &name, &size); i++) {
                        printf("'%s' %11" B_PRId32 " %11" B_PRIuSIZE "  %s\n",
                                resource_type(type), id, size,
                                name != NULL && name[0] != '\0' ? name : "(no name)");
                }
        }
};


// WriteFileState
struct WriteFileState : State {
        WriteFileState()
                :
                fOutputFilePath(kDefaultOutputFile),
                fResources(NULL),
                fInclusionPattern(NULL),
                fExclusionPattern(NULL)
        {
        }

        virtual ~WriteFileState()
        {
                _FlushOutput();
        }

        virtual void SetOutput(const char *path)
        {
                _FlushOutput();

                fOutputFilePath = path;
        }

        virtual void ProcessInput(const char *path)
        {
                // open the file for reading
                BFile file;
                status_t error = file.SetTo(path, B_READ_ONLY);
                if (error != B_OK) {
                        fprintf(stderr, "Error: Failed to open input file \"%s\": %s\n",
                                path, strerror(error));
                        exit(1);
                }

                // open the resources
                BResources resources;
                error = resources.SetTo(&file, false);
                if (error != B_OK) {
                        if (error == B_ERROR) {
                                fprintf(stderr, "Error: Input file \"%s\" is not a resource "
                                "file.\n", path);
                        } else {
                                fprintf(stderr, "Error: Failed to read resources from input "
                                        "file \"%s\": %s\n", path, strerror(error));
                        }

                        exit(1);
                }
                resources.PreloadResourceType();

                // add resources
                type_code type;
                int32 id;
                const char *name;
                size_t size;
                for (int32 i = 0;
                         resources.GetResourceInfo(i, &type, &id, &name, &size);
                         i++) {
                        // load the resource
                        const void *data = resources.LoadResource(type, id, &size);
                        if (!data) {
                                fprintf(stderr, "Error: Failed to read resources from input "
                                        "file \"%s\".\n", path);

                                exit(1);
                        }

                        // add it
                        MemoryResourceDataSource dataSource(data, size, false);
                        AddResource(ResourceID(type, id), name, &dataSource);
                }
        }

        virtual void SetInclusionPattern(const ResourceID &pattern)
        {
                if (!fInclusionPattern)
                        fInclusionPattern = new ResourceID;
                *fInclusionPattern = pattern;
        }

        virtual void SetExclusionPattern(const ResourceID &pattern)
        {
                if (!fExclusionPattern)
                        fExclusionPattern = new ResourceID;
                *fExclusionPattern = pattern;
        }

        virtual void AddResource(const ResourceID &id, const char *name,
                ResourceDataSource *dataSource)
        {
                _PrepareOutput();

                // filter resource
                if ((fInclusionPattern && !fInclusionPattern->Matches(id))
                        || (fExclusionPattern && fExclusionPattern->Matches(id))) {
                        // not included or explicitly excluded
                        return;
                }

                // get resource data
                const void *data;
                size_t size;
                dataSource->GetData(data, size);

                // add the resource
                status_t error = fResources->AddResource(id.type, id.id, data, size,
                        name);
                if (error != B_OK) {
                        fprintf(stderr, "Error: Failed to add resource type '%s', ID %"
                                B_PRId32 " to output file \"%s\": %s\n", resource_type(id.type),
                                id.id, fOutputFilePath.c_str(), strerror(error));
                        exit(1);
                }
        }

private:
        void _FlushOutput()
        {
                if (fResources) {
                        status_t error = fResources->Sync();
                        if (error != B_OK) {
                                fprintf(stderr, "Error: Failed to write resources to output "
                                        "file \"%s\": %s\n", fOutputFilePath.c_str(),
                                        strerror(error));

                                exit(1);
                        }

                        delete fResources;
                        fResources = NULL;
                }
        }

        void _PrepareOutput()
        {
                if (fResources)
                        return;

                // open the file for writing
                BFile file;
                status_t error = file.SetTo(fOutputFilePath.c_str(),
                        B_READ_WRITE | B_CREATE_FILE);
                if (error != B_OK) {
                        fprintf(stderr, "Error: Failed to open output file \"%s\": %s\n",
                                fOutputFilePath.c_str(), strerror(error));
                        exit(1);
                }

                // open the resources
                fResources = new BResources;
                error = fResources->SetTo(&file, true);
                if (error != B_OK) {
                        fprintf(stderr, "Error: Failed to init resources for output "
                                "file \"%s\": %s\n", fOutputFilePath.c_str(), strerror(error));

                        exit(1);
                }
        }

private:
        string          fOutputFilePath;
        BResources      *fResources;
        ResourceID      *fInclusionPattern;
        ResourceID      *fExclusionPattern;
};


// Command
struct Command {
        Command()
        {
        }

        virtual ~Command()
        {
        }

        virtual void Do(State *state) = 0;
};


// SetOutputCommand
struct SetOutputCommand : Command {
        SetOutputCommand(const char *path)
                :
                Command(),
                fPath(path)
        {
        }

        virtual void Do(State *state)
        {
                state->SetOutput(fPath.c_str());
        }

private:
        string  fPath;
};


// ProcessInputCommand
struct ProcessInputCommand : Command {
        ProcessInputCommand(const char *path)
                :
                Command(),
                fPath(path)
        {
        }

        virtual void Do(State *state)
        {
                state->ProcessInput(fPath.c_str());
        }

private:
        string  fPath;
};


// SetResourcePatternCommand
struct SetResourcePatternCommand : Command {
        SetResourcePatternCommand(const ResourceID &pattern, bool inclusion)
                :
                Command(),
                fPattern(pattern),
                fInclusion(inclusion)
        {
        }

        virtual void Do(State *state)
        {
                if (fInclusion)
                        state->SetInclusionPattern(fPattern);
                else
                        state->SetExclusionPattern(fPattern);
        }

private:
        ResourceID      fPattern;
        bool            fInclusion;
};


// AddResourceCommand
struct AddResourceCommand : Command {
        AddResourceCommand(const ResourceID &id, const char *name,
                        ResourceDataSource *dataSource)
                :
                Command(),
                fID(id),
                fHasName(name),
                fDataSource(dataSource)
        {
                if (fHasName)
                        fName = name;
        }

        virtual ~AddResourceCommand()
        {
                delete fDataSource;
        }

        virtual void Do(State *state)
        {
                state->AddResource(fID, (fHasName ? fName.c_str() : NULL),
                        fDataSource);
                fDataSource->Flush();
        }

private:
        ResourceID                      fID;
        string                          fName;
        bool                            fHasName;
        ResourceDataSource      *fDataSource;
};


// print_usage
static void
print_usage(bool error)
{
        // get command name
        const char *commandName = NULL;
        if (kArgc > 0) {
                if (const char *lastSlash = strchr(kArgv[0], '/'))
                        commandName = lastSlash + 1;
                else
                        commandName = kArgv[0];
        }

        if (!commandName || commandName[0] == '\0')
                commandName = kCommandName;

        // print usage
        fprintf((error ? stderr : stdout), kUsage, commandName, commandName,
                commandName);
}


// print_usage_and_exit
static void
print_usage_and_exit(bool error)
{
        print_usage(error);
        exit(error ? 1 : 0);
}


// next_arg
static const char *
next_arg(int &argi, bool optional = false)
{
        if (argi >= kArgc) {
                if (!optional)
                        print_usage_and_exit(true);
                return NULL;
        }

        return kArgv[argi++];
}


// parse_resource_id
static void
parse_resource_id(const char *toParse, ResourceID &resourceID,
        const char **name = NULL)
{
        int len = strlen(toParse);

        // type
        if (len < 4)
                print_usage_and_exit(true);

        resourceID.type = ((int32)toParse[0] << 24) | ((int32)toParse[1] << 16)
                | ((int32)toParse[2] << 8) | (int32)toParse[3];

        if (toParse[4] == '\0') {
                // if a name can be provided, the ID is mandatory
                if (name)
                        print_usage_and_exit(true);

                resourceID.id = 0;
                resourceID.wildcardID = true;
                return;
        }

        if (toParse[4] != ':')
                print_usage_and_exit(true);

        toParse += 5;
        len -= 5;

        // ID
        bool negative = false;
        if (*toParse == '-') {
                negative = true;
                toParse++;
                len--;
        }

        if (*toParse < '0' || *toParse > '9')
                print_usage_and_exit(true);

        int id = 0;
        while (*toParse >= '0' && *toParse <= '9') {
                id = 10 * id + (*toParse - '0');
                toParse++;
                len--;
        }

        resourceID.wildcardID = false;
        resourceID.id = (negative ? -id : id);

        if (*toParse == '\0') {
                if (name)
                        *name = kDefaultResourceName;
                return;
        }

        if (*toParse != ':')
                print_usage_and_exit(true);

        // the remainder is name
        *name = toParse + 1;
}


// main
int
main(int argc, const char *const *argv)
{
        kArgc = argc;
        kArgv = argv;

        if (argc < 2)
                print_usage_and_exit(true);

        BList commandList;

        // parse the arguments
        bool noMoreOptions = false;
        bool list = false;
        bool noList = false;
        bool hasInputFiles = false;
        for (int argi = 1; argi < argc; ) {
                const char *arg = argv[argi++];
                if (!noMoreOptions && arg[0] == '-') {
                        if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0)
                                print_usage_and_exit(false);

                        if (strlen(arg) != 2)
                                print_usage_and_exit(true);

                        switch (arg[1]) {
                                case 'a':
                                {
                                        noList = true;

                                        // get id
                                        const char *typeString = next_arg(argi);
                                        ResourceID resourceID;
                                        const char *name = NULL;
                                        parse_resource_id(typeString, resourceID, &name);

                                        // get data
                                        const char *file = next_arg(argi);
                                        ResourceDataSource *dataSource;
                                        if (strcmp(file, "-s") == 0) {
                                                const char *data = next_arg(argi);
                                                dataSource = new MemoryResourceDataSource(data, false);

                                        } else {
                                                dataSource = new FileResourceDataSource(file);
                                        }

                                        // add command
                                        Command *command = new AddResourceCommand(resourceID,
                                                name, dataSource);
                                        commandList.AddItem(command);

                                        break;
                                }

                                case 'd':
                                {
                                        noList = true;

                                        // get pattern
                                        const char *typeString = next_arg(argi);
                                        ResourceID pattern;
                                        parse_resource_id(typeString, pattern);

                                        // add command
                                        Command *command = new SetResourcePatternCommand(pattern,
                                                false);
                                        commandList.AddItem(command);

                                        break;
                                }

                                case 'l':
                                {
                                        list = true;
                                        break;
                                }

                                case 'o':
                                {
                                        noList = true;

                                        // get file name
                                        const char *out = next_arg(argi);

                                        // add command
                                        Command *command = new SetOutputCommand(out);
                                        commandList.AddItem(command);

                                        break;
                                }

                                case 'x':
                                {
                                        noList = true;

                                        // get pattern
                                        const char *typeString = next_arg(argi);
                                        ResourceID pattern;
                                        parse_resource_id(typeString, pattern);

                                        // add command
                                        Command *command = new SetResourcePatternCommand(pattern,
                                                true);
                                        commandList.AddItem(command);

                                        break;
                                }

                                case '-':
                                        noMoreOptions = true;
                                        break;

                                default:
                                        print_usage_and_exit(true);
                                        break;
                        }

                } else {
                        // input file
                        hasInputFiles = true;
                        Command *command = new ProcessInputCommand(arg);
                        commandList.AddItem(command);
                }
        }

        // don't allow "-l" together with other comands or without at least one
        // input file
        if ((list && noList) || (list && !hasInputFiles))
                print_usage_and_exit(true);

        // create a state
        State *state;
        if (list)
                state = new ListState();
        else
                state = new WriteFileState();

        // process commands
        for (int32 i = 0; Command *command = (Command*)commandList.ItemAt(i); i++)
                command->Do(state);

        // delete state (will flush resources)
        delete state;

        return 0;
}