/*
 * Copyright (c) 2017-2020 [Ribose Inc](https://www.ribose.com).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1.  Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *
 * 2.  Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include <stdlib.h>
#include <stdio.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#include <string.h>
#else
#include "uniwin.h"
#endif
#include <sys/stat.h>
#include <stdarg.h>
#include <errno.h>
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_LIMITS_H
#include <limits.h>
#endif
#include <rnp/rnp_def.h>
#include "rnp.h"
#include "stream-common.h"
#include "types.h"
#include "file-utils.h"
#include "crypto/mem.h"
#include <algorithm>
#include <memory>

bool
src_read(pgp_source_t *src, void *buf, size_t len, size_t *readres)
{
    size_t              left = len;
    size_t              read;
    pgp_source_cache_t *cache = src->cache;
    bool                readahead = cache ? cache->readahead : false;

    if (src->error) {
        return false;
    }

    if (src->eof || (len == 0)) {
        *readres = 0;
        return true;
    }

    // Do not read more then available if source size is known
    if (src->knownsize && (src->readb + len > src->size)) {
        len = src->size - src->readb;
        left = len;
        readahead = false;
    }

    // Check whether we have cache and there is data inside
    if (cache && (cache->len > cache->pos)) {
        read = cache->len - cache->pos;
        if (read >= len) {
            memcpy(buf, &cache->buf[cache->pos], len);
            cache->pos += len;
            goto finish;
        } else {
            memcpy(buf, &cache->buf[cache->pos], read);
            cache->pos += read;
            buf = (uint8_t *) buf + read;
            left = len - read;
        }
    }

    // If we got here then we have empty cache or no cache at all
    while (left > 0) {
        if (left > sizeof(cache->buf) || !readahead || !cache) {
            // If there is no cache or chunk is larger then read directly
            if (!src->read(src, buf, left, &read)) {
                src->error = 1;
                return false;
            }
            if (!read) {
                src->eof = 1;
                len = len - left;
                goto finish;
            }
            left -= read;
            buf = (uint8_t *) buf + read;
        } else {
            // Try to fill the cache to avoid small reads
            if (!src->read(src, &cache->buf[0], sizeof(cache->buf), &read)) {
                src->error = 1;
                return false;
            }
            if (!read) {
                src->eof = 1;
                len = len - left;
                goto finish;
            } else if (read < left) {
                memcpy(buf, &cache->buf[0], read);
                left -= read;
                buf = (uint8_t *) buf + read;
            } else {
                memcpy(buf, &cache->buf[0], left);
                cache->pos = left;
                cache->len = read;
                goto finish;
            }
        }
    }

finish:
    src->readb += len;
    if (src->knownsize && (src->readb == src->size)) {
        src->eof = 1;
    }
    *readres = len;
    return true;
}

bool
src_read_eq(pgp_source_t *src, void *buf, size_t len)
{
    size_t res = 0;
    return src_read(src, buf, len, &res) && (res == len);
}

bool
src_peek(pgp_source_t *src, void *buf, size_t len, size_t *peeked)
{
    pgp_source_cache_t *cache = src->cache;
    if (src->error) {
        return false;
    }
    if (!cache || (len > sizeof(cache->buf))) {
        return false;
    }
    if (src->eof) {
        *peeked = 0;
        return true;
    }

    size_t read = 0;
    bool   readahead = cache->readahead;
    // Do not read more then available if source size is known
    if (src->knownsize && (src->readb + len > src->size)) {
        len = src->size - src->readb;
        readahead = false;
    }

    if (cache->len - cache->pos >= len) {
        if (buf) {
            memcpy(buf, &cache->buf[cache->pos], len);
        }
        *peeked = len;
        return true;
    }

    if (cache->pos > 0) {
        memmove(&cache->buf[0], &cache->buf[cache->pos], cache->len - cache->pos);
        cache->len -= cache->pos;
        cache->pos = 0;
    }

    while (cache->len < len) {
        read = readahead ? sizeof(cache->buf) - cache->len : len - cache->len;
        if (src->knownsize && (src->readb + read > src->size)) {
            read = src->size - src->readb;
        }
        if (!src->read(src, &cache->buf[cache->len], read, &read)) {
            src->error = 1;
            return false;
        }
        if (!read) {
            if (buf) {
                memcpy(buf, &cache->buf[0], cache->len);
            }
            *peeked = cache->len;
            return true;
        }
        cache->len += read;
        if (cache->len >= len) {
            if (buf) {
                memcpy(buf, cache->buf, len);
            }
            *peeked = len;
            return true;
        }
    }
    return false;
}

bool
src_peek_eq(pgp_source_t *src, void *buf, size_t len)
{
    size_t res = 0;
    return src_peek(src, buf, len, &res) && (res == len);
}

void
src_skip(pgp_source_t *src, size_t len)
{
    if (src->cache && (src->cache->len - src->cache->pos >= len)) {
        src->readb += len;
        src->cache->pos += len;
        return;
    }

    size_t  res = 0;
    uint8_t sbuf[16];
    if (len < sizeof(sbuf)) {
        (void) src_read(src, sbuf, len, &res);
        return;
    }
    if (src_eof(src)) {
        return;
    }

    void *buf = calloc(1, std::min((size_t) PGP_INPUT_CACHE_SIZE, len));
    if (!buf) {
        src->error = 1;
        return;
    }

    while (len && !src_eof(src)) {
        if (!src_read(src, buf, std::min((size_t) PGP_INPUT_CACHE_SIZE, len), &res)) {
            break;
        }
        len -= res;
    }
    free(buf);
}

rnp_result_t
src_finish(pgp_source_t *src)
{
    rnp_result_t res = RNP_SUCCESS;
    if (src->finish) {
        res = src->finish(src);
    }

    return res;
}

bool
src_error(const pgp_source_t *src)
{
    return src->error;
}

bool
src_eof(pgp_source_t *src)
{
    if (src->eof) {
        return true;
    }
    /* Error on stream read is NOT considered as eof. See src_error(). */
    uint8_t check;
    size_t  read = 0;
    return src_peek(src, &check, 1, &read) && (read == 0);
}

void
src_close(pgp_source_t *src)
{
    if (src->close) {
        src->close(src);
    }

    if (src->cache) {
        free(src->cache);
        src->cache = NULL;
    }
}

bool
src_skip_eol(pgp_source_t *src)
{
    uint8_t eol[2];
    size_t  read;

    if (!src_peek(src, eol, 2, &read) || !read) {
        return false;
    }
    if (eol[0] == '\n') {
        src_skip(src, 1);
        return true;
    }
    if ((read == 2) && (eol[0] == '\r') && (eol[1] == '\n')) {
        src_skip(src, 2);
        return true;
    }
    return false;
}

bool
src_peek_line(pgp_source_t *src, char *buf, size_t len, size_t *readres)
{
    size_t scan_pos = 0;
    size_t inc = 64;
    len = len - 1;

    do {
        size_t to_peek = scan_pos + inc;
        to_peek = to_peek > len ? len : to_peek;
        inc = inc * 2;

        /* inefficient, each time we again read from the beginning */
        if (!src_peek(src, buf, to_peek, readres)) {
            return false;
        }

        /* we continue scanning where we stopped previously */
        for (; scan_pos < *readres; scan_pos++) {
            if (buf[scan_pos] == '\n') {
                if ((scan_pos > 0) && (buf[scan_pos - 1] == '\r')) {
                    scan_pos--;
                }
                buf[scan_pos] = '\0';
                *readres = scan_pos;
                return true;
            }
        }
        if (*readres < to_peek) {
            return false;
        }
    } while (scan_pos < len);
    return false;
}

bool
init_src_common(pgp_source_t *src, size_t paramsize)
{
    memset(src, 0, sizeof(*src));
    src->cache = (pgp_source_cache_t *) calloc(1, sizeof(*src->cache));
    if (!src->cache) {
        RNP_LOG("cache allocation failed");
        return false;
    }
    src->cache->readahead = true;
    if (!paramsize) {
        return true;
    }
    src->param = calloc(1, paramsize);
    if (!src->param) {
        RNP_LOG("param allocation failed");
        free(src->cache);
        src->cache = NULL;
        return false;
    }
    return true;
}

typedef struct pgp_source_file_param_t {
    int fd;
} pgp_source_file_param_t;

static bool
file_src_read(pgp_source_t *src, void *buf, size_t len, size_t *readres)
{
    pgp_source_file_param_t *param = (pgp_source_file_param_t *) src->param;
    if (!param) {
        return false;
    }

    int64_t rres = read(param->fd, buf, len);
    if (rres < 0) {
        return false;
    }
    *readres = rres;
    return true;
}

static void
file_src_close(pgp_source_t *src)
{
    pgp_source_file_param_t *param = (pgp_source_file_param_t *) src->param;
    if (param) {
        if (src->type == PGP_STREAM_FILE) {
            close(param->fd);
        }
        free(src->param);
        src->param = NULL;
    }
}

static rnp_result_t
init_fd_src(pgp_source_t *src, int fd, uint64_t *size)
{
    if (!init_src_common(src, sizeof(pgp_source_file_param_t))) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    pgp_source_file_param_t *param = (pgp_source_file_param_t *) src->param;
    param->fd = fd;
    src->read = file_src_read;
    src->close = file_src_close;
    src->type = PGP_STREAM_FILE;
    src->size = size ? *size : 0;
    src->knownsize = !!size;

    return RNP_SUCCESS;
}

rnp_result_t
init_file_src(pgp_source_t *src, const char *path)
{
    int         fd;
    struct stat st;

    if (rnp_stat(path, &st) != 0) {
        RNP_LOG("can't stat '%s'", path);
        return RNP_ERROR_READ;
    }

    /* read call may succeed on directory depending on OS type */
    if (S_ISDIR(st.st_mode)) {
        RNP_LOG("source is directory");
        return RNP_ERROR_BAD_PARAMETERS;
    }

    int flags = O_RDONLY;
#ifdef HAVE_O_BINARY
    flags |= O_BINARY;
#else
#ifdef HAVE__O_BINARY
    flags |= _O_BINARY;
#endif
#endif
    fd = rnp_open(path, flags, 0);

    if (fd < 0) {
        RNP_LOG("can't open '%s'", path);
        return RNP_ERROR_READ;
    }
    uint64_t     size = st.st_size;
    rnp_result_t ret = init_fd_src(src, fd, &size);
    if (ret) {
        close(fd);
    }
    return ret;
}

rnp_result_t
init_stdin_src(pgp_source_t *src)
{
    pgp_source_file_param_t *param;

    if (!init_src_common(src, sizeof(pgp_source_file_param_t))) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    param = (pgp_source_file_param_t *) src->param;
    param->fd = 0;
    src->read = file_src_read;
    src->close = file_src_close;
    src->type = PGP_STREAM_STDIN;

    return RNP_SUCCESS;
}

typedef struct pgp_source_mem_param_t {
    const void *memory;
    bool        free;
    size_t      len;
    size_t      pos;
} pgp_source_mem_param_t;

typedef struct pgp_dest_mem_param_t {
    unsigned maxalloc;
    unsigned allocated;
    void *   memory;
    bool     free;
    bool     discard_overflow;
    bool     secure;
} pgp_dest_mem_param_t;

static bool
mem_src_read(pgp_source_t *src, void *buf, size_t len, size_t *read)
{
    pgp_source_mem_param_t *param = (pgp_source_mem_param_t *) src->param;
    if (!param) {
        return false;
    }

    if (len > param->len - param->pos) {
        len = param->len - param->pos;
    }
    memcpy(buf, (uint8_t *) param->memory + param->pos, len);
    param->pos += len;
    *read = len;
    return true;
}

static void
mem_src_close(pgp_source_t *src)
{
    pgp_source_mem_param_t *param = (pgp_source_mem_param_t *) src->param;
    if (param) {
        if (param->free) {
            free((void *) param->memory);
        }
        free(src->param);
        src->param = NULL;
    }
}

rnp_result_t
init_mem_src(pgp_source_t *src, const void *mem, size_t len, bool free)
{
    if (!mem && len) {
        return RNP_ERROR_NULL_POINTER;
    }
    /* this is actually double buffering, but then src_peek will fail */
    if (!init_src_common(src, sizeof(pgp_source_mem_param_t))) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    pgp_source_mem_param_t *param = (pgp_source_mem_param_t *) src->param;
    param->memory = mem;
    param->len = len;
    param->pos = 0;
    param->free = free;
    src->read = mem_src_read;
    src->close = mem_src_close;
    src->finish = NULL;
    src->size = len;
    src->knownsize = 1;
    src->type = PGP_STREAM_MEMORY;

    return RNP_SUCCESS;
}

static bool
null_src_read(pgp_source_t *src, void *buf, size_t len, size_t *read)
{
    return false;
}

rnp_result_t
init_null_src(pgp_source_t *src)
{
    memset(src, 0, sizeof(*src));
    src->read = null_src_read;
    src->type = PGP_STREAM_NULL;
    src->error = true;
    return RNP_SUCCESS;
}

rnp_result_t
read_mem_src(pgp_source_t *src, pgp_source_t *readsrc)
{
    pgp_dest_t   dst;
    rnp_result_t ret;

    if ((ret = init_mem_dest(&dst, NULL, 0))) {
        return ret;
    }

    if ((ret = dst_write_src(readsrc, &dst))) {
        goto done;
    }

    if ((ret = init_mem_src(src, mem_dest_own_memory(&dst), dst.writeb, true))) {
        goto done;
    }

    ret = RNP_SUCCESS;
done:
    dst_close(&dst, true);
    return ret;
}

rnp_result_t
file_to_mem_src(pgp_source_t *src, const char *filename)
{
    pgp_source_t fsrc = {};
    rnp_result_t res = RNP_ERROR_GENERIC;

    if ((res = init_file_src(&fsrc, filename))) {
        return res;
    }

    res = read_mem_src(src, &fsrc);
    src_close(&fsrc);

    return res;
}

const void *
mem_src_get_memory(pgp_source_t *src, bool own)
{
    if (src->type != PGP_STREAM_MEMORY) {
        RNP_LOG("wrong function call");
        return NULL;
    }

    if (!src->param) {
        return NULL;
    }

    pgp_source_mem_param_t *param = (pgp_source_mem_param_t *) src->param;
    if (own) {
        param->free = false;
    }
    return param->memory;
}

bool
init_dst_common(pgp_dest_t *dst, size_t paramsize)
{
    memset(dst, 0, sizeof(*dst));
    dst->werr = RNP_SUCCESS;
    if (!paramsize) {
        return true;
    }
    /* allocate param */
    dst->param = calloc(1, paramsize);
    if (!dst->param) {
        RNP_LOG("allocation failed");
    }
    return dst->param;
}

void
dst_write(pgp_dest_t *dst, const void *buf, size_t len)
{
    /* we call write function only if all previous calls succeeded */
    if ((len > 0) && (dst->write) && (dst->werr == RNP_SUCCESS)) {
        /* if cache non-empty and len will overflow it then fill it and write out */
        if ((dst->clen > 0) && (dst->clen + len > sizeof(dst->cache))) {
            memcpy(dst->cache + dst->clen, buf, sizeof(dst->cache) - dst->clen);
            buf = (uint8_t *) buf + sizeof(dst->cache) - dst->clen;
            len -= sizeof(dst->cache) - dst->clen;
            dst->werr = dst->write(dst, dst->cache, sizeof(dst->cache));
            dst->writeb += sizeof(dst->cache);
            dst->clen = 0;
            if (dst->werr != RNP_SUCCESS) {
                return;
            }
        }

        /* here everything will fit into the cache or cache is empty */
        if (dst->no_cache || (len > sizeof(dst->cache))) {
            dst->werr = dst->write(dst, buf, len);
            if (!dst->werr) {
                dst->writeb += len;
            }
        } else {
            memcpy(dst->cache + dst->clen, buf, len);
            dst->clen += len;
        }
    }
}

void
dst_printf(pgp_dest_t *dst, const char *format, ...)
{
    char    buf[2048];
    size_t  len;
    va_list ap;

    va_start(ap, format);
    len = vsnprintf(buf, sizeof(buf), format, ap);
    va_end(ap);

    if (len >= sizeof(buf)) {
        RNP_LOG("too long dst_printf");
        len = sizeof(buf) - 1;
    }
    dst_write(dst, buf, len);
}

void
dst_flush(pgp_dest_t *dst)
{
    if ((dst->clen > 0) && (dst->write) && (dst->werr == RNP_SUCCESS)) {
        dst->werr = dst->write(dst, dst->cache, dst->clen);
        dst->writeb += dst->clen;
        dst->clen = 0;
    }
}

rnp_result_t
dst_finish(pgp_dest_t *dst)
{
    rnp_result_t res = RNP_SUCCESS;

    if (!dst->finished) {
        /* flush write cache in the dst */
        dst_flush(dst);
        if (dst->finish) {
            res = dst->finish(dst);
        }
        dst->finished = true;
    }

    return res;
}

void
dst_close(pgp_dest_t *dst, bool discard)
{
    if (!discard && !dst->finished) {
        dst_finish(dst);
    }

    if (dst->close) {
        dst->close(dst, discard);
    }
}

typedef struct pgp_dest_file_param_t {
    int         fd;
    int         errcode;
    bool        overwrite;
    std::string path;
} pgp_dest_file_param_t;

static rnp_result_t
file_dst_write(pgp_dest_t *dst, const void *buf, size_t len)
{
    pgp_dest_file_param_t *param = (pgp_dest_file_param_t *) dst->param;

    if (!param) {
        RNP_LOG("wrong param");
        return RNP_ERROR_BAD_PARAMETERS;
    }

    /* we assyme that blocking I/O is used so everything is written or error received */
    ssize_t ret = write(param->fd, buf, len);
    if (ret < 0) {
        param->errcode = errno;
        RNP_LOG("write failed, error %d", param->errcode);
        return RNP_ERROR_WRITE;
    } else {
        param->errcode = 0;
        return RNP_SUCCESS;
    }
}

static void
file_dst_close(pgp_dest_t *dst, bool discard)
{
    pgp_dest_file_param_t *param = (pgp_dest_file_param_t *) dst->param;
    if (!param) {
        return;
    }

    if (dst->type == PGP_STREAM_FILE) {
        close(param->fd);
        if (discard) {
            rnp_unlink(param->path.c_str());
        }
    }

    delete param;
    dst->param = NULL;
}

static rnp_result_t
init_fd_dest(pgp_dest_t *dst, int fd, const char *path)
{
    if (!init_dst_common(dst, 0)) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    try {
        std::unique_ptr<pgp_dest_file_param_t> param(new pgp_dest_file_param_t());
        param->path = path;
        param->fd = fd;
        dst->param = param.release();
    } catch (const std::exception &e) {
        RNP_LOG("%s", e.what());
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    dst->write = file_dst_write;
    dst->close = file_dst_close;
    dst->type = PGP_STREAM_FILE;
    return RNP_SUCCESS;
}

rnp_result_t
init_file_dest(pgp_dest_t *dst, const char *path, bool overwrite)
{
    /* check whether file/dir already exists */
    struct stat st;
    if (!rnp_stat(path, &st)) {
        if (!overwrite) {
            RNP_LOG("file already exists: '%s'", path);
            return RNP_ERROR_WRITE;
        }

        /* if we are overwriting empty directory then should first remove it */
        if (S_ISDIR(st.st_mode)) {
            if (rmdir(path) == -1) {
                RNP_LOG("failed to remove directory: error %d", errno);
                return RNP_ERROR_BAD_PARAMETERS;
            }
        }
    }

    int flags = O_WRONLY | O_CREAT;
    flags |= overwrite ? O_TRUNC : O_EXCL;
#ifdef HAVE_O_BINARY
    flags |= O_BINARY;
#else
#ifdef HAVE__O_BINARY
    flags |= _O_BINARY;
#endif
#endif
    int fd = rnp_open(path, flags, S_IRUSR | S_IWUSR);
    if (fd < 0) {
        RNP_LOG("failed to create file '%s'. Error %d.", path, errno);
        return RNP_ERROR_WRITE;
    }

    rnp_result_t res = init_fd_dest(dst, fd, path);
    if (res) {
        close(fd);
    }
    return res;
}

#define TMPDST_SUFFIX ".rnp-tmp.XXXXXX"

static rnp_result_t
file_tmpdst_finish(pgp_dest_t *dst)
{
    pgp_dest_file_param_t *param = (pgp_dest_file_param_t *) dst->param;
    if (!param) {
        return RNP_ERROR_BAD_PARAMETERS;
    }

    /* close the file */
    close(param->fd);
    param->fd = -1;

    /* rename the temporary file */
    if (param->path.size() < strlen(TMPDST_SUFFIX)) {
        return RNP_ERROR_BAD_PARAMETERS;
    }
    try {
        /* remove suffix so we have required path */
        std::string origpath(param->path.begin(), param->path.end() - strlen(TMPDST_SUFFIX));
        /* check if file already exists */
        struct stat st;
        if (!rnp_stat(origpath.c_str(), &st)) {
            if (!param->overwrite) {
                RNP_LOG("target path already exists");
                return RNP_ERROR_BAD_STATE;
            }
#ifdef _WIN32
            /* rename() call on Windows fails if destination exists */
            else {
                rnp_unlink(origpath.c_str());
            }
#endif

            /* we should remove dir if overwriting, file will be unlinked in rename call */
            if (S_ISDIR(st.st_mode) && rmdir(origpath.c_str())) {
                RNP_LOG("failed to remove directory");
                return RNP_ERROR_BAD_STATE;
            }
        }

        if (rnp_rename(param->path.c_str(), origpath.c_str())) {
            RNP_LOG("failed to rename temporary path to target file: %s", strerror(errno));
            return RNP_ERROR_BAD_STATE;
        }
        return RNP_SUCCESS;
    } catch (const std::exception &e) {
        RNP_LOG("%s", e.what());
        return RNP_ERROR_BAD_STATE;
    }
}

static void
file_tmpdst_close(pgp_dest_t *dst, bool discard)
{
    pgp_dest_file_param_t *param = (pgp_dest_file_param_t *) dst->param;
    if (!param) {
        return;
    }

    /* we close file in finish function, except the case when some error occurred */
    if (!dst->finished && (dst->type == PGP_STREAM_FILE)) {
        close(param->fd);
        if (discard) {
            rnp_unlink(param->path.c_str());
        }
    }

    delete param;
    dst->param = NULL;
}

rnp_result_t
init_tmpfile_dest(pgp_dest_t *dst, const char *path, bool overwrite)
{
    try {
        std::string tmp = std::string(path) + std::string(TMPDST_SUFFIX);
        /* make sure tmp.data() is zero-terminated */
        tmp.push_back('\0');
#if defined(HAVE_MKSTEMP) && !defined(_WIN32)
        int fd = mkstemp(&tmp[0]);
#else
        int fd = rnp_mkstemp(&tmp[0]);
#endif
        if (fd < 0) {
            RNP_LOG("failed to create temporary file with template '%s'. Error %d.",
                    tmp.c_str(),
                    errno);
            return RNP_ERROR_WRITE;
        }
        rnp_result_t res = init_fd_dest(dst, fd, tmp.c_str());
        if (res) {
            close(fd);
            return res;
        }
    } catch (const std::exception &e) {
        RNP_LOG("%s", e.what());
        return RNP_ERROR_BAD_STATE;
    }

    /* now let's change some parameters to handle temporary file correctly */
    pgp_dest_file_param_t *param = (pgp_dest_file_param_t *) dst->param;
    param->overwrite = overwrite;
    dst->finish = file_tmpdst_finish;
    dst->close = file_tmpdst_close;
    return RNP_SUCCESS;
}

rnp_result_t
init_stdout_dest(pgp_dest_t *dst)
{
    rnp_result_t res = init_fd_dest(dst, STDOUT_FILENO, "");
    if (res) {
        return res;
    }
    dst->type = PGP_STREAM_STDOUT;
    return RNP_SUCCESS;
}

static rnp_result_t
mem_dst_write(pgp_dest_t *dst, const void *buf, size_t len)
{
    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;
    if (!param) {
        return RNP_ERROR_BAD_PARAMETERS;
    }

    /* checking whether we need to realloc or discard extra bytes */
    if (param->discard_overflow && (dst->writeb >= param->allocated)) {
        return RNP_SUCCESS;
    }
    if (param->discard_overflow && (dst->writeb + len > param->allocated)) {
        len = param->allocated - dst->writeb;
    }

    if (dst->writeb + len > param->allocated) {
        if ((param->maxalloc > 0) && (dst->writeb + len > param->maxalloc)) {
            RNP_LOG("attempt to alloc more then allowed");
            return RNP_ERROR_OUT_OF_MEMORY;
        }

        /* round up to the page boundary and do it exponentially */
        size_t alloc = ((dst->writeb + len) * 2 + 4095) / 4096 * 4096;
        if ((param->maxalloc > 0) && (alloc > param->maxalloc)) {
            alloc = param->maxalloc;
        }

        void *newalloc = param->secure ? calloc(1, alloc) : realloc(param->memory, alloc);
        if (!newalloc) {
            return RNP_ERROR_OUT_OF_MEMORY;
        }
        if (param->secure && param->memory) {
            memcpy(newalloc, param->memory, dst->writeb);
            secure_clear(param->memory, dst->writeb);
            free(param->memory);
        }
        param->memory = newalloc;
        param->allocated = alloc;
    }

    memcpy((uint8_t *) param->memory + dst->writeb, buf, len);
    return RNP_SUCCESS;
}

static void
mem_dst_close(pgp_dest_t *dst, bool discard)
{
    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;
    if (!param) {
        return;
    }

    if (param->free) {
        if (param->secure) {
            secure_clear(param->memory, param->allocated);
        }
        free(param->memory);
    }
    free(param);
    dst->param = NULL;
}

rnp_result_t
init_mem_dest(pgp_dest_t *dst, void *mem, unsigned len)
{
    pgp_dest_mem_param_t *param;

    if (!init_dst_common(dst, sizeof(*param))) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }

    param = (pgp_dest_mem_param_t *) dst->param;

    param->maxalloc = len;
    param->allocated = mem ? len : 0;
    param->memory = mem;
    param->free = !mem;
    param->secure = false;

    dst->write = mem_dst_write;
    dst->close = mem_dst_close;
    dst->type = PGP_STREAM_MEMORY;
    dst->werr = RNP_SUCCESS;
    dst->no_cache = true;

    return RNP_SUCCESS;
}

void
mem_dest_discard_overflow(pgp_dest_t *dst, bool discard)
{
    if (dst->type != PGP_STREAM_MEMORY) {
        RNP_LOG("wrong function call");
        return;
    }

    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;
    if (param) {
        param->discard_overflow = discard;
    }
}

void *
mem_dest_get_memory(pgp_dest_t *dst)
{
    if (dst->type != PGP_STREAM_MEMORY) {
        RNP_LOG("wrong function call");
        return NULL;
    }

    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;

    if (param) {
        return param->memory;
    }

    return NULL;
}

void *
mem_dest_own_memory(pgp_dest_t *dst)
{
    if (dst->type != PGP_STREAM_MEMORY) {
        RNP_LOG("wrong function call");
        return NULL;
    }

    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;

    if (!param) {
        RNP_LOG("null param");
        return NULL;
    }

    dst_finish(dst);

    if (param->free) {
        if (!dst->writeb) {
            free(param->memory);
            param->memory = NULL;
            return param->memory;
        }
        /* it may be larger then required - let's truncate */
        void *newalloc = realloc(param->memory, dst->writeb);
        if (!newalloc) {
            return NULL;
        }
        param->memory = newalloc;
        param->allocated = dst->writeb;
        param->free = false;
        return param->memory;
    }

    /* in this case we should copy the memory */
    void *res = malloc(dst->writeb);
    if (res) {
        memcpy(res, param->memory, dst->writeb);
    }
    return res;
}

void
mem_dest_secure_memory(pgp_dest_t *dst, bool secure)
{
    if (!dst || (dst->type != PGP_STREAM_MEMORY)) {
        RNP_LOG("wrong function call");
        return;
    }
    pgp_dest_mem_param_t *param = (pgp_dest_mem_param_t *) dst->param;
    if (param) {
        param->secure = secure;
    }
}

static rnp_result_t
null_dst_write(pgp_dest_t *dst, const void *buf, size_t len)
{
    return RNP_SUCCESS;
}

static void
null_dst_close(pgp_dest_t *dst, bool discard)
{
    ;
}

rnp_result_t
init_null_dest(pgp_dest_t *dst)
{
    dst->param = NULL;
    dst->write = null_dst_write;
    dst->close = null_dst_close;
    dst->type = PGP_STREAM_NULL;
    dst->writeb = 0;
    dst->clen = 0;
    dst->werr = RNP_SUCCESS;
    dst->no_cache = true;

    return RNP_SUCCESS;
}

rnp_result_t
dst_write_src(pgp_source_t *src, pgp_dest_t *dst, uint64_t limit)
{
    const size_t bufsize = PGP_INPUT_CACHE_SIZE;
    uint8_t *    readbuf = (uint8_t *) malloc(bufsize);
    if (!readbuf) {
        return RNP_ERROR_OUT_OF_MEMORY;
    }
    rnp_result_t res = RNP_SUCCESS;
    try {
        size_t   read;
        uint64_t totalread = 0;

        while (!src->eof) {
            if (!src_read(src, readbuf, bufsize, &read)) {
                res = RNP_ERROR_GENERIC;
                break;
            }
            if (!read) {
                continue;
            }
            totalread += read;
            if (limit && totalread > limit) {
                res = RNP_ERROR_GENERIC;
                break;
            }
            if (dst) {
                dst_write(dst, readbuf, read);
                if (dst->werr) {
                    RNP_LOG("failed to output data");
                    res = RNP_ERROR_WRITE;
                    break;
                }
            }
        }
    } catch (...) {
        free(readbuf);
        throw;
    }
    free(readbuf);
    if (res || !dst) {
        return res;
    }
    dst_flush(dst);
    return dst->werr;
}
