root/src/bin/diff_zip.cpp
/*
 * Copyright 2008, Ingo Weinhold, ingo_weinhold@gmx.de. All rights reserved.
 *
 * Distributed under the terms of the MIT License.
 */

#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <map>
#include <string>


using std::string;
using std::map;


class Directory;
class Node;

static Node* create_node(Directory* parent, const string& name,
        const struct stat& st);


enum diff_status {
        DIFF_UNCHANGED,
        DIFF_REMOVED,
        DIFF_CHANGED,
        DIFF_ERROR
};


class EntryWriter {
public:
        EntryWriter(int fd)
                : fFD(fd)
        {
        }

        void Write(const char* entry)
        {
                write(fFD, entry, strlen(entry));
                write(fFD, "\n", 1);
        }

private:
        int     fFD;
};


class Node {
public:
        Node(Directory* parent, const string& name, const struct stat& st)
                : fParent(parent),
                  fName(name),
                  fStat(st)
        {
        }

        virtual ~Node()
        {
        }

        Directory* Parent() const               { return fParent; }
        const string& Name() const              { return fName; }
        const struct stat& Stat() const { return fStat; }

        string Path() const;

        bool DoStat(struct stat& st) const
        {
                string path(Path());
                if (lstat(path.c_str(), &st) != 0)
                        return false;
                return true;
        }

        virtual bool Scan()
        {
                return true;
        }

        virtual diff_status CollectDiffEntries(EntryWriter* out) const
        {
                string path(Path());
                struct stat st;

                diff_status status = DiffEntry(path, st);
                if (status == DIFF_CHANGED)
                        out->Write(path.c_str());

                return status;
        }

        virtual void Dump(int level) const
        {
                printf("%*s%s\n", level, "", fName.c_str());
        }

protected:
        diff_status DiffEntry(const string& path, struct stat& st) const
        {
                if (lstat(path.c_str(), &st) == 0) {
                        if (st.st_mode != fStat.st_mode
                                || st.st_mtime != fStat.st_mtime
                                || st.st_size != fStat.st_size) {
                                return DIFF_CHANGED;
                        }
                        return DIFF_UNCHANGED;
                } else if (errno == ENOENT) {
                        // that's OK, the entry was removed
                        return DIFF_REMOVED;
                } else {
                        // some error
                        fprintf(stderr, "Error: Failed to stat \"%s\": %s\n",
                                path.c_str(), strerror(errno));
                        return DIFF_ERROR;
                }
        }

private:
        Directory*      fParent;
        string          fName;
        struct stat     fStat;
};


class Directory : public Node {
public:
        Directory(Directory* parent, const string& name, const struct stat& st)
                : Node(parent, name, st),
                  fEntries()
        {
        }

        void AddEntry(const char* name, Node* node)
        {
                fEntries[name] = node;
        }

        virtual bool Scan()
        {
                string path(Path());
                DIR* dir = opendir(path.c_str());
                if (dir == NULL) {
                        fprintf(stderr, "Error: Failed to open directory \"%s\": %s\n",
                                path.c_str(), strerror(errno));
                        return false;
                }

                errno = 0;
                while (dirent* entry = readdir(dir)) {
                        if (strcmp(entry->d_name, ".") == 0
                                || strcmp(entry->d_name, "..") == 0) {
                                continue;
                        }

                        string entryPath = path + '/' + entry->d_name;
                        struct stat st;
                        if (lstat(entryPath.c_str(), &st) != 0) {
                                fprintf(stderr, "Error: Failed to stat entry \"%s\": %s\n",
                                        entryPath.c_str(), strerror(errno));
                                closedir(dir);
                                return false;
                        }

                        Node* node = create_node(this, entry->d_name, st);
                        fEntries[entry->d_name] = node;

                        errno = 0;
                }

                if (errno != 0) {
                        fprintf(stderr, "Error: Failed to read directory \"%s\": %s\n",
                                path.c_str(), strerror(errno));
                        closedir(dir);
                        return false;
                }

                closedir(dir);

                // recursively scan the entries
                for (EntryMap::iterator it = fEntries.begin(); it != fEntries.end();
                                ++it) {
                        Node* node = it->second;
                        if (!node->Scan())
                                return false;
                }

                return true;
        }

        virtual diff_status CollectDiffEntries(EntryWriter* out) const
        {
                string path(Path());
                struct stat st;

                diff_status status = DiffEntry(path, st);
                if (status == DIFF_REMOVED || status == DIFF_ERROR)
                        return status;

                // we print it only if it is no longer a directory
                if (!S_ISDIR(st.st_mode)) {
                        out->Write(path.c_str());
                        return DIFF_CHANGED;
                }

                // iterate through the "new" entries
                DIR* dir = opendir(path.c_str());
                if (dir == NULL) {
                        fprintf(stderr, "Error: Failed to open directory \"%s\": %s\n",
                                path.c_str(), strerror(errno));
                        return DIFF_ERROR;
                }

                errno = 0;
                while (dirent* entry = readdir(dir)) {
                        if (strcmp(entry->d_name, ".") == 0
                                || strcmp(entry->d_name, "..") == 0) {
                                continue;
                        }

                        EntryMap::const_iterator it = fEntries.find(entry->d_name);
                        if (it == fEntries.end()) {
                                // new entry
                                string entryPath = path + "/" + entry->d_name;
                                out->Write(entryPath.c_str());
                        } else {
                                // old entry -- recurse
                                diff_status entryStatus = it->second->CollectDiffEntries(out);
                                if (entryStatus == DIFF_ERROR) {
                                        closedir(dir);
                                        return status;
                                }
                                if (entryStatus != DIFF_UNCHANGED)
                                        status = DIFF_CHANGED;
                        }

                        errno = 0;
                }

                if (errno != 0) {
                        fprintf(stderr, "Error: Failed to read directory \"%s\": %s\n",
                                path.c_str(), strerror(errno));
                        closedir(dir);
                        return DIFF_ERROR;
                }

                closedir(dir);
                return status;
        }

        virtual void Dump(int level) const
        {
                Node::Dump(level);

                for (EntryMap::const_iterator it = fEntries.begin();
                                it != fEntries.end(); ++it) {
                        it->second->Dump(level + 1);
                }
        }


private:
        typedef map<string, Node*> EntryMap;

        EntryMap        fEntries;
};


string
Node::Path() const
{
        if (fParent == NULL)
                return fName;

        return fParent->Path() + '/' + fName;
}


static Node*
create_node(Directory* parent, const string& name, const struct stat& st)
{
        if (S_ISDIR(st.st_mode))
                return new Directory(parent, name, st);
        return new Node(parent, name, st);
}


int
main(int argc, const char* const* argv)
{
        // the paths are listed after a "--" argument
        int argi = 1;
        for (; argi < argc; argi++) {
                if (strcmp(argv[argi], "--") == 0)
                        break;
        }

        if (argi + 1 >= argc) {
                fprintf(stderr, "Usage: %s <zip arguments> ... -- <paths>\n", argv[0]);
                exit(1);
        }

        int zipArgCount = argi;
        const char* const* paths = argv + argi + 1;
        int pathCount = argc - argi - 1;

        // snapshot the hierarchy
        Node** rootNodes = new Node*[pathCount];

        for (int i = 0; i < pathCount; i++) {
                const char* path = paths[i];
                struct stat st;
                if (lstat(path, &st) != 0) {
                        fprintf(stderr, "Error: Failed to stat \"%s\": %s\n", path,
                                strerror(errno));
                        exit(1);
                }

                rootNodes[i] = create_node(NULL, path, st);
                if (!rootNodes[i]->Scan())
                        exit(1);
        }

        // create a temp file
        char tmpFileName[64];
        sprintf(tmpFileName, "/tmp/diff_zip_%d_XXXXXX", (int)getpid());
        int tmpFileFD = mkstemp(tmpFileName);
        if (tmpFileFD < 0) {
                fprintf(stderr, "Error: Failed create temp file: %s\n",
                        strerror(errno));
                exit(1);
        }
        unlink(tmpFileName);

        // wait
        {
                printf("Waiting for changes. Press RETURN to continue...");
                fflush(stdout);
                char buffer[32];
                fgets(buffer, sizeof(buffer), stdin);
        }

        EntryWriter tmpFile(tmpFileFD);

        for (int i = 0; i < pathCount; i++) {
                if (rootNodes[i]->CollectDiffEntries(&tmpFile) == DIFF_ERROR)
                        exit(1);
        }

        // dup the temp file FD to our stdin and exec()
        dup2(tmpFileFD, 0);
        close(tmpFileFD);
        lseek(0, 0, SEEK_SET);

        char** zipArgs = new char*[zipArgCount + 2];
        zipArgs[0] = strdup("zip");
        zipArgs[1] = strdup("-@");
        for (int i = 1; i < zipArgCount; i++)
                zipArgs[i + 1] = strdup(argv[i]);
        zipArgs[zipArgCount + 1] = NULL;

        execvp("zip", zipArgs);

        fprintf(stderr, "Error: Failed exec*() zip: %s\n", strerror(errno));
        delete[] rootNodes;

        return 1;
}