// Copyright (C) 2010-2014 David Sugar, Tycho Softworks.
// Copyright (C) 2015-2020 Cherokees of Idaho.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <ucommon/secure.h>
#include <sys/stat.h>

using namespace ucommon;

static shell::flagopt helpflag('h',"--help",    _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::stringopt tag('t', "--tag", _TEXT("tag annotation"), "text", "");
static shell::stringopt algo('c', "--cipher", _TEXT("cipher method (aes256)"), "method", "aes256");
static shell::flagopt decode('d', "--decode", _TEXT("decode archive"));
static shell::stringopt hash('h', "--digest", _TEXT("digest method (sha)"), "method", "sha");
static shell::flagopt noheader('n', "--no-header", _TEXT("without wrapper"));
static shell::stringopt out('o', "--output", _TEXT("output file"), "filename", "-");
static shell::flagopt quiet('q', "--quiet", _TEXT("quiet operation"));
static shell::flagopt recursive('R', "--recursive", _TEXT("recursive directory scan"));
static shell::flagopt altrecursive('r', NULL, NULL);
static shell::flagopt hidden('s', "--hidden", _TEXT("include hidden files"));
static shell::flagopt yes('y', "--overwrite", _TEXT("overwrite existing files"));

static bool binary = false;
static int exit_code = 0;
static const char *argv0 = "car";
static uint8_t frame[48], cbuf[48];
static cipher_t cipher;
static FILE *output = stdout;
static enum {d_text, d_file, d_scan, d_init} decoder = d_init;
static unsigned frames;

static void report(const char *path, int code)
{
    const char *err = _TEXT("i/o error");

    switch(code) {
    case EACCES:
    case EPERM:
        err = _TEXT("permission denied");
        break;
    case EROFS:
        err = _TEXT("read-only file system");
        break;
    case ENODEV:
    case ENOENT:
        err = _TEXT("no such file or directory");
        break;
    case ENOTDIR:
        err = _TEXT("not a directory");
        break;
    case ENOTEMPTY:
        err = _TEXT("directory not empty");
        break;
    case ENOSPC:
        err = _TEXT("no space left on device");
        break;
    case EBADF:
    case ENAMETOOLONG:
        err = _TEXT("bad file path");
        break;
    case EBUSY:
    case EINPROGRESS:
        err = _TEXT("file or directory busy");
        break;
    case EINTR:
        err = _TEXT("operation interrupted");
        break;
    case EISDIR:
        err = _TEXT("is a directory");
        break;
#ifdef  ELOOP
    case ELOOP:
        err = _TEXT("too many sym links");
        break;
#endif
    }

    if(path)
        shell::printf("%s: %s: %s\n", argv0, path, err);
    else
        shell::errexit(1, "*** %s: %s\n", argv0, err);

    exit_code = 1;
}

static bool encode(const char *path, FILE *fp, size_t offset = 0)
{
    memset(frame, 0, sizeof(frame));

    size_t count = fread(frame + offset, 1, sizeof(frame) - offset, fp);
    char buffer[128];

    if(ferror(fp)) {
        report(path, errno);
        return false;
    }

    if(count)
        count += offset;

    // add padd value for last frame...
    if(count < sizeof(frame))
        frame[sizeof(frame) - 1] = (char)(count - offset);

    // cipher it...
    size_t encoded = cipher.put(frame, sizeof(frame));
    if(encoded != sizeof(frame)) {
        report(path, EINTR);
        return false;
    }

    if(binary)
        fwrite(cbuf, sizeof(cbuf), 1, output);
    else {
        String::b64encode(buffer, cbuf, 48);
        fprintf(output, "%s\n", buffer);
    }

    // return status
    if(count == sizeof(frame))
        return true;

    return false;
}

static void encodestream(void)
{

    size_t offset = 6;

    if(fsys::is_tty(shell::input()))
        fputs("car: type your message\n", stderr);

    memset(frame, 0, sizeof(frame));

    for(;;) {
        if(!encode("-", stdin, offset))
            break;
        offset = 0;
    }

    if(!binary && !is(noheader))
        fprintf(output, "-----END CAR STREAM-----\n");
}

static void header(void)
{
    binary = true;

    memset(frame, 0, sizeof(frame));
    String::set((char *)frame, 6, ".car");
    frame[5] = 1;
    frame[4] = 0xff;
    String::set((char *)frame + 6, sizeof(frame) - 6, *tag);
    fwrite(frame, sizeof(frame), 1, output);
}

static void encodefile(const char *path, const char *name)
{
    char buffer[128];

    fsys::fileinfo_t ino;

    fsys::info(path, &ino);

    FILE *fp = fopen(path, "r");
    if(!fp) {
        report(name, errno);
        return;
    }

    lsb_setlong(frame, ino.st_size);
    frame[4] = 1;
    frame[5] = 0;
    String::set((char *)(frame + 6), sizeof(frame) - 6, name);

    cipher.put(frame, sizeof(frame));

    if(binary)
        fwrite(cbuf, sizeof(cbuf), 1, output);
    else {
        String::b64encode(buffer, cbuf, 48);
        fprintf(output, "%s\n", buffer);
    }

    for(;;) {
        if(!encode(name, fp))
            break;
    }
    fclose(fp);
}

static void final(void)
{
    size_t size;

    switch(decoder) {
    case d_init:
        return;
    case d_scan:
        cipher.put(frame, sizeof(frame));
        if(cbuf[4] > 0)
            return;
        size = cbuf[sizeof(frame) - 1];
        if(size)
            fwrite(cbuf + 6, size, 1, output);
        break;
    default:
        decoder = d_scan;
        cipher.put(frame, sizeof(frame));
        size = cbuf[sizeof(frame) - 1];
        if(size)
            fwrite(cbuf, size, 1, output);
    }
}

static void process(void)
{
    string_t path;
    int key;
    char *cp;

    switch(decoder) {
    case d_init:
        decoder = d_scan;
        return;
    case d_scan:
        cipher.put(frame, sizeof(frame));
        if(cbuf[4] == 0xff) // header...
            break;
        if(!cbuf[4]) {      // if message
            fwrite(cbuf + 6, sizeof(frame) - 6, 1, output);
            decoder = d_text;
            break;
        }
        decoder = d_file;
        frames = lsb_getlong(cbuf) / sizeof(frame);
        path = str((char *)(cbuf + 6));
        cp = strrchr((char *)(cbuf + 6), '/');
        if(cp) {
            *cp = 0;
            dir::create((char *)(cbuf + 6), 0640);
        }
        if(fsys::is_dir(*path))
            shell::errexit(8, "*** %s: %s: %s\n",
                argv0, *path, _TEXT("output is directory"));

        if(fsys::is_file(*path) && !is(yes)) {
            string_t prompt = str("overwrite ") + path + " <y/n>? ";
            if(is(quiet))
                key = 0;
            else
                key = shell::inkey(prompt);
            switch(key)
            {
            case 'y':
            case 'Y':
                printf("y\n");
                break;
            default:
                printf("n\n");
            case 0:
                shell::errexit(8, "*** %s: %s: %s\n",
                    argv0, *path, _TEXT("will not overwrite"));
            }
        }

        output = fopen(*path, "w");
        if(!output)
            shell::errexit(8, "*** %s: %s: %s\n",
                argv0, *path, _TEXT("cannot create"));
        if(!is(quiet))
            printf("decoding %s...\n", *path);
        break;
    case d_file:
        if(!frames) {
            final();
            break;
        }
        --frames;
    default:
        cipher.put(frame, sizeof(frame));
        fwrite(cbuf, sizeof(frame), 1, output);
    }
}

static void binarydecode(FILE *fp, const char *path)
{
    char buffer[48];

    memset(frame, 0, sizeof(frame));
    memset(buffer, 0, sizeof(buffer));
    if(fread(frame, sizeof(frame), 1, fp) < 1)
        shell::errexit(6, "*** %s: %s: %s\n",
            argv0, path, _TEXT("cannot read archive"));

    if(!eq((char *)frame, ".car", 4) || (frame[4] != 0xff))
        shell::errexit(6, "*** %s: %s: %s\n",
            argv0, path, _TEXT("not a cryptographic archive"));

    for(;;) {
        if(feof(fp)) {
            final();
            return;
        }

        if(ferror(fp)) {
            exit_code = errno;
            report(path, exit_code);
            return;
        }

        if(fread(buffer, sizeof(buffer), 1, fp) < 1)
            continue;
        process();
        memcpy(frame, buffer, sizeof(frame));
    }
}

static void streamdecode(FILE *fp, const char *path)
{
    char buffer[128];
    for(;;) {
        if(NULL == fgets(buffer, sizeof(buffer), fp) || feof(fp)) {
            shell::errexit(5, "*** %s: %s: %s\n",
                argv0, path, _TEXT("no archive found"));
        }
        if(ferror(fp)) {
            exit_code = errno;
            report(path, exit_code);
            return;
        }
        if(eq("-----BEGIN CAR STREAM-----\n", buffer))
            break;
    }
    for(;;) {
        if(feof(fp)) {
            final();
            return;
        }

        if(ferror(fp)) {
            exit_code = errno;
            report(path, exit_code);
            return;
        }

        if(fgets(buffer, sizeof(buffer), fp) == NULL)
            continue;

        if(eq("-----END CAR STREAM-----\n", buffer)) {
            final();
            return;
        }

        // ignore extra headers...
        if(strstr(buffer, ": "))
            continue;

        process();
        unsigned len = String::b64decode(frame, buffer, 48);
        if(len < 48) {
            report(path, EINTR);
            return;
        }
    }
}

static void scan(string_t path, string_t prefix)
{
    char filename[128];
    string_t filepath;
    string_t name;
    string_t subdir;
    dir_t dir(path);

    while(is(dir) && dir.read(filename, sizeof(filename))) {
        if(*filename == '.' && (filename[1] == '.' || !filename[1]))
            continue;

        if(*filename == '.' && !is(hidden))
            continue;

        filepath = str(path) + str("/") + str(filename);
        if(prefix[0])
            name ^= prefix + str("/") + str(filename);
        else
            name ^= str(filename);

        if(fsys::is_dir(filepath)) {
            if(is(recursive) || is(altrecursive))
                scan(filepath, name);
            else
                report(*filepath, EISDIR);
        }
        else
            encodefile(*filepath, *name);
    }
}

int main(int argc, char **argv)
{
    shell::bind("car");
    shell args(argc, argv);
    argv0 = args.argv0();
    unsigned count = 0;
    char passphrase[256];
    char confirm[256];
    const char *ext;

    argv0 = args.argv0();

    if(is(helpflag) || is(althelp)) {
        printf("%s\n", _TEXT("Usage: car [options] path..."));
        printf("%s\n\n", _TEXT("Crytographic archiver"));
        printf("%s\n", _TEXT("Options:"));
        shell::help();
        printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
        return 0;
    }

    if(!secure::init())
        shell::errexit(1, "*** %s: %s\n", argv0, _TEXT("not supported"));

    if(!Digest::has(*hash))
        shell::errexit(2, "*** %s: %s: %s\n",
            argv0, *hash, _TEXT("unknown or unsupported digest method"));

    if(!Cipher::has(*algo))
        shell::errexit(2, "*** %s: %s: %s\n",
            argv0, *algo, _TEXT("unknown or unsupported cipher method"));

    shell::getpass("passphrase: ", passphrase, sizeof(passphrase));
    shell::getpass("confirm: ", confirm, sizeof(confirm));

    if(!eq(passphrase, confirm))
        shell::errexit(3, "*** %s: %s\n",
            argv0, _TEXT("passphrase does not match confirmation"));

    // set key and cleanup buffers...
    skey_t key(*algo, *hash, passphrase);
    memset(passphrase, 0, sizeof(passphrase));
    memset(confirm, 0, sizeof(confirm));
    memset(cbuf, 0, sizeof(cbuf));

    if(is(decode))
        cipher.set(&key, Cipher::DECRYPT, cbuf, sizeof(cbuf));
    else
        cipher.set(&key, Cipher::ENCRYPT, cbuf, sizeof(cbuf));

    if(is(decode) && !args()) {
        streamdecode(stdin, "-");
        goto end;
    }

    if(is(decode) && args() > 1)
        shell::errexit(3, "*** %s: %s\n", argv0, _TEXT("specify only one file for decoder"));

    if(is(decode)) {
        FILE *fp = fopen(args[0], "r");
        if(!fp)
            shell::errexit(7, "*** %s: %s: %s\n",
                argv0, args[0], _TEXT("cannot open or access"));

        const char *ext = strrchr(args[0], '.');
        if(eq_case(ext, ".car"))
            binarydecode(fp, args[0]);
        else
            streamdecode(fp, args[0]);
        fclose(fp);
        goto end;
    }

    if(!eq(*out, "-")) {
        output = fopen(*out, "w");
        if(!output) {
            shell::errexit(4, "*** %s: %s: %s\n",
                argv0, *out, _TEXT("cannot create"));
        }
    }

    // if we are outputting to a car file, do it in binary
    ext = strrchr(*out, '.');
    if(eq_case(ext, ".car"))
        header();

    if(!binary && !is(noheader)) {
        fprintf(output, "-----BEGIN CAR STREAM-----\n");
        if(tag)
            fprintf(output, "Tag: %s\n", *tag);
    }

    if(!args()) {
        encodestream();
        goto end;
    }

    while(count < args()) {
        if(fsys::is_dir(args[count]))
            scan(str(args[count++]), "");
        else {
            const char *cp = args[count++];
            const char *ep = strrchr(cp, '/');
            if(!ep)
                ep = strrchr(cp, '\\');
            if(ep)
                ++ep;
            else
                ep = cp;
            encodefile(cp, ep);
        }
    }

    if(!binary && !is(noheader))
        fprintf(output, "-----END CAR STREAM-----\n");

end:
    return exit_code;
}

