/*
 * tpm_ioctl  --  ioctl utility for the TPM
 *
 * Authors: David Safford <safford@us.ibm.com>
 *          Stefan Berger <stefanb@us.ibm.com>
 *
 * (c) Copyright IBM Corporation 2014 - 2016.
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 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.
 *
 * Neither the names of the IBM Corporation nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * 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
 * HOLDER 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.
 */
/*
 * tool for using 'swtpm cuse' ioctls
 *
 * cuse_tpm_ioctl [ -c | -i | -s | -e | -l num | -C | -v ] devicepath
 *     -c get ptm capabilities
 *     -i do a hardware TPM_Init
 *     -s shutdown tpm_server_cuse
 *     -e get tpmEstablished bit
 *     -l set locality to num
 *     -h hash the given data
 *     -v store volatile data to file
 *     -C cancel an ongoing TPM command
 */

#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stddef.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netdb.h>
#include <sys/param.h>
#include <signal.h>
#include <poll.h>

#include <swtpm/tpm_ioctl.h>

#include <libtpms/tpm_error.h>

#include "sys_dependencies.h"
#include "swtpm.h"

#define DEFAULT_TCP_PORT 6546

#define devtoh32(is_chardev, x) (is_chardev ? x : be32toh(x))
#define htodev32(is_chardev, x) (is_chardev ? x : htobe32(x))

#define devtoh64(is_chardev, x) (is_chardev ? x : be64toh(x))
#define htodev64(is_chardev, x) (is_chardev ? x : htobe64(x))

/* for OpenBSD */
#ifndef _IOC_NRSHIFT
# define _IOC_NRSHIFT 0
#endif
#ifndef _IOC_NRMASK
# define _IOC_NRMASK 255
#endif

/* poll timeout that takes into account a busy swtpm creating a key */
#define DEFAULT_POLL_TIMEOUT 10000 /* ms */

static unsigned long ioctl_to_cmd(unsigned long ioctlnum)
{
    /* the ioctl number contains the command number - 1 */
    return ((ioctlnum >> _IOC_NRSHIFT) & _IOC_NRMASK) + 1;
}

/*
 * ctrlcmd - send a control command
 *
 * This function returns -1 on on error with errno indicating the error.
 * In case an ioctl is used, it returns 0 on success; otherwise
 * it returns the number of bytes received in the response.
 */
static int ctrlcmd(int fd, unsigned long cmd, void *msg, size_t msg_len_in,
                   size_t msg_len_out)
{
    struct stat statbuf;
    int n;

    n = fstat(fd, &statbuf);
    if (n < 0) {
        fprintf(stderr, "fstat failed: %s\n", strerror(errno));
        return -1;
    }

    if ((statbuf.st_mode & S_IFMT) == S_IFCHR) {
        n = ioctl(fd, cmd, msg);
    } else {
        uint32_t cmd_no = htobe32(ioctl_to_cmd(cmd));
        struct iovec iov[2] = {
            {
                .iov_base = &cmd_no,
                .iov_len = sizeof(cmd_no),
            }, {
                .iov_base = msg,
                .iov_len = msg_len_in,
            },
        };

        n = writev(fd, iov, 2);
        if (n > 0) {
            if (msg_len_out > 0) {
                struct pollfd fds = {
                    .fd = fd,
                    .events = POLLIN,
                };
                n = poll(&fds, 1, DEFAULT_POLL_TIMEOUT);
                if (n == 1) {
                    n = read(fd, msg, msg_len_out);
                } else if (n == 0) {
                    n = -1;
                    errno = ETIMEDOUT;
                }
            } else {
                /* we read 0 bytes */
                n = 0;
            }
        }
    }
    return n;
}

/*
 * Do PTM_HASH_START, PTM_HASH_DATA, PTM_HASH_END on the
 * data.
 */
static int do_hash_start_data_end(int fd, bool is_chardev, const char *input)
{
    ptm_res res;
    int n;
    size_t idx;
    ptm_hdata hdata;
    size_t inputlen;

    memset(&hdata, 0, sizeof(hdata));

    if (!input) {
        fprintf(stderr,
                "Input buffer for hashing must not be NULL.\n");
        return 1;
    }

    /* hash string given on command line */
    n = ctrlcmd(fd, PTM_HASH_START, &res, 0, sizeof(res));
    if (n < 0) {
        fprintf(stderr,
                "Could not execute ioctl PTM_HASH_START: "
                "%s\n", strerror(errno));
        return 1;
    }
    if (devtoh32(is_chardev, res) != 0) {
        fprintf(stderr,
                "TPM result from PTM_HASH_START: 0x%x\n",
                devtoh32(is_chardev, res));
        return 1;
    }
    inputlen = strlen(input);

    if (inputlen == 1 && input[0] == '-') {
        /* read data from stdin */
        while (1) {
            idx = 0;
            int c = 0;

            while (idx < sizeof(hdata.u.req.data)) {
                c = fgetc(stdin);
                if (c == EOF)
                    break;

                hdata.u.req.data[idx] = (char)c;
                idx++;
            }
            hdata.u.req.length = htodev32(is_chardev, idx);

            n = ctrlcmd(fd, PTM_HASH_DATA, &hdata,
                        offsetof(ptm_hdata, u.req.data) + idx,
                        sizeof(hdata.u.resp));

            res = devtoh32(is_chardev, hdata.u.resp.tpm_result);
            if (n < 0 || res != 0 || c == EOF)
                break;
        }
    } else {
        idx = 0;
        while (idx < inputlen) {
            size_t tocopy = inputlen - idx;

            if (tocopy > sizeof(hdata.u.req.data))
                tocopy = sizeof(hdata.u.req.data);

            hdata.u.req.length = htodev32(is_chardev, tocopy);
            memcpy(hdata.u.req.data, &input[idx], tocopy);
            idx += tocopy;

            n = ctrlcmd(fd, PTM_HASH_DATA, &hdata,
                        offsetof(ptm_hdata, u.req.data) + tocopy,
                        sizeof(hdata.u.resp));

            res = devtoh32(is_chardev, hdata.u.resp.tpm_result);
            if (n < 0 || res != 0)
                break;
        }
    }
    if (n < 0) {
        fprintf(stderr,
                "Could not execute ioctl PTM_HASH_DATA: "
                "%s\n", strerror(errno));
        return 1;
    }
    if (res != 0) {
        fprintf(stderr,
               "TPM result from PTM_HASH_DATA: 0x%x\n", res);
        return 1;
    }
    n = ctrlcmd(fd, PTM_HASH_END, &res, 0, sizeof(res));
    if (n < 0) {
        fprintf(stderr,
                "Could not execute ioctl PTM_HASH_END: "
                "%s\n", strerror(errno));
        return 1;
    }
    if (devtoh32(is_chardev, res) != 0) {
        fprintf(stderr,
                "TPM result from PTM_HASH_END: 0x%x\n",
                devtoh32(is_chardev, res));
        return 1;
    }

    return 0;
}

static uint32_t get_blobtype(const char *blobname)
{
    if (!blobname)
        return 0;
    if (!strcmp(blobname, "permanent"))
        return PTM_BLOB_TYPE_PERMANENT;
    if (!strcmp(blobname, "volatile"))
        return PTM_BLOB_TYPE_VOLATILE;
    if (!strcmp(blobname, "savestate"))
        return PTM_BLOB_TYPE_SAVESTATE;
    return 0;
}

/*
 * do_save_state_blob: Get a state blob from the TPM and store it into the
 *                     given file
 * @fd: file descriptor to talk to the TPM
 * @is_chardev: whether @fd is a character device using ioctl
 * @blobtype: the name of the blobtype
 * @filename: name of the file to store the blob into
 *
 */
static int do_save_state_blob(int fd, bool is_chardev, const char *blobtype,
                              const char *filename, size_t buffersize)
{
    int file_fd;
    ptm_res res;
    ptm_getstate pgs;
    uint16_t offset;
    ssize_t numbytes, remain = -1;
    bool had_error;
    int n;
    uint32_t bt;
    unsigned char *buffer =  NULL;
    uint32_t recvd_bytes;

    memset(&pgs, 0, sizeof(pgs));

    bt = get_blobtype(blobtype);
    if (!bt) {
        fprintf(stderr,
                "Unknown TPM state type '%s'", blobtype);
        return 1;
    }

    file_fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR);
    if (file_fd < 0) {
        fprintf(stderr,
                "Could not open file '%s' for writing: %s\n",
                filename, strerror(errno));
        return 1;
    }

    if (!is_chardev)
        buffersize = 4096;

    had_error = false;
    offset = 0;

    while (true) {
        /* fill out request every time since response may change it */
        pgs.u.req.state_flags = htodev32(is_chardev, PTM_STATE_FLAG_DECRYPTED);
        pgs.u.req.type = htodev32(is_chardev, bt);
        pgs.u.req.offset = htodev32(is_chardev, offset);

        n = ctrlcmd(fd, PTM_GET_STATEBLOB, &pgs, sizeof(pgs.u.req),
                    sizeof(pgs.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute ioctl PTM_GET_STATEBLOB: "
                    "%s\n", strerror(errno));
            had_error = true;
            break;
        }
        res = devtoh32(is_chardev, pgs.u.resp.tpm_result);
        if (res != 0 && (res & TPM_NON_FATAL) == 0) {
            fprintf(stderr,
                    "TPM result from PTM_GET_STATEBLOB: 0x%x\n",
                    res);
            had_error = true;
            break;
        }

        if (remain == -1)
            remain = devtoh32(is_chardev, pgs.u.resp.totlength);

        if (!is_chardev) {
            /* we receive a part of the chunk */
            recvd_bytes = n - offsetof(ptm_getstate, u.resp.data);
            if ((int32_t)recvd_bytes < 0) {
                fprintf(stderr,
                        "Too few bytes in response");
                had_error = true;
                break;
            }
        } else {
            recvd_bytes =
                MIN(devtoh32(is_chardev, pgs.u.resp.length),
                    sizeof(pgs.u.resp.data));
        }

        numbytes = write(file_fd, pgs.u.resp.data, recvd_bytes);
        if (numbytes < 0 || (uint32_t)numbytes != recvd_bytes) {
            fprintf(stderr,
                    "Could not write to file '%s': %s\n",
                    filename, strerror(errno));
            had_error = true;
            break;
        }
        remain -= recvd_bytes;

        /* done when the last byte was received */
        if (offset + recvd_bytes >= devtoh32(is_chardev, pgs.u.resp.totlength))
            break;

        if (buffersize) {
            /* continue with the read interface */
            buffer = malloc(buffersize);
            if (!buffer) {
                fprintf(stderr,
                        "Could not allocate buffer with %zu bytes.",
                        buffersize);
                had_error = true;
                break;
            }

            while (true) {
                /* read from TPM */
                n = read(fd, buffer, buffersize);
                if (n < 0) {
                    fprintf(stderr,
                            "Could not read from TPM: %s\n",
                            strerror(errno));
                    had_error = true;
                    break;
                }
                remain -= n;

                numbytes = write(file_fd, buffer, n);
                if (numbytes < 0) {
                    fprintf(stderr,
                            "Could not write to file '%s': %s\n",
                            filename, strerror(errno));
                    had_error = true;
                    break;
                }
                if (remain <= 0)
                    break;
            }

            break;
        } else {
            offset += devtoh32(is_chardev, pgs.u.resp.length);
        }
    }

    close(file_fd);

    free(buffer);

    if (had_error)
        return 1;

    if (remain != 0) {
        fprintf(stderr, "Unexpected number of remaining bytes: %zd\n", remain);
        return 1;
    }

    return 0;
}

/*
 * do_load_state_blob: Load a TPM state blob from a file and load it into the
 *                     TPM
 * @fd: file descriptor to talk to the TPM
 * @is_chardev: whether @fd is a character device
 * @blobtype: the name of the blobtype
 * @filename: name of the file to store the blob into
 * @buffersize: the size of the buffer to use via write() interface
 */
static int do_load_state_blob(int fd, bool is_chardev, const char *blobtype,
                              const char *filename,
                              size_t buffersize)
{
    int file_fd;
    ptm_res res;
    ptm_setstate pss;
    ssize_t numbytes;
    bool had_error;
    int n;
    uint32_t bt;
    unsigned char *buffer = NULL;
    struct stat statbuf;

    bt = get_blobtype(blobtype);
    if (!bt) {
        fprintf(stderr,
                "Unknown TPM state type '%s'", blobtype);
        return 1;
    }

    if (!filename) {
        fprintf(stderr,
                "Filename parameter must not be NULL\n");
        return 1;
    }

    file_fd = open(filename, O_RDONLY);
    if (file_fd < 0) {
        fprintf(stderr,
                "Could not open file '%s' for reading: %s\n",
                filename, strerror(errno));
        return 1;
    }

    if (!is_chardev) {
        n = fstat(file_fd, &statbuf);
        if (n < 0) {
            fprintf(stderr,
                    "Could not stat file '%s': %s\n",
                    filename, strerror(errno));
            close(file_fd);
            return 1;
        }
        buffersize = statbuf.st_size;
    }

    had_error = false;

    if (!buffersize) {
        /* use only the ioctl interface for the transfer */
        while (true) {
            size_t returnsize;

            /* fill out request every time since response may change it */
            pss.u.req.state_flags = htodev32(is_chardev, 0);
            pss.u.req.type = htodev32(is_chardev, bt);

            numbytes = read(file_fd, pss.u.req.data, sizeof(pss.u.req.data));
            if (numbytes < 0) {
                fprintf(stderr,
                        "Could not read from file '%s': %s\n",
                        filename, strerror(errno));
               had_error = true;
               break;
            }
            pss.u.req.length = htodev32(is_chardev, (uint32_t)numbytes);

            /* the returnsize is zero on all intermediate packets */
            returnsize = ((size_t)numbytes < sizeof(pss.u.req.data))
                         ? sizeof(pss) : 0;

            n = ctrlcmd(fd, PTM_SET_STATEBLOB, &pss,
                        offsetof(ptm_setstate, u.req.data) + numbytes,
                        returnsize);
            if (n < 0) {
                fprintf(stderr,
                        "Could not execute ioctl PTM_SET_STATEBLOB: "
                        "%s\n", strerror(errno));
                had_error = true;
                break;
            }
            res = devtoh32(is_chardev, pss.u.resp.tpm_result);
            if (res != 0) {
                fprintf(stderr,
                        "TPM result from PTM_SET_STATEBLOB: 0x%x\n",
                        res);
                had_error = true;
                break;
            }
            if ((size_t)numbytes < sizeof(pss.u.req.data))
                break;
        }
    } else {
        buffer = malloc(buffersize);
        if (!buffer) {
            fprintf(stderr,
                    "Could not allocate buffer with %zu bytes.",
                    buffersize);
            had_error = true;
            goto cleanup;
        }

        memset(&pss, 0, sizeof(pss));
        pss.u.req.state_flags = htodev32(is_chardev, 0);
        pss.u.req.type = htodev32(is_chardev, bt);
        /* will use write interface */
        if (is_chardev) {
            pss.u.req.length = htodev32(is_chardev, 0);
        } else {
            pss.u.req.length = htodev32(is_chardev, buffersize);
        }

        n = ctrlcmd(fd, PTM_SET_STATEBLOB, &pss,
                    offsetof(ptm_setstate, u.req.data) + 0,
                    0);
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute ioctl PTM_SET_STATEBLOB: "
                    "%s\n", strerror(errno));
            had_error = 1;
            goto cleanup;
        }
        res = devtoh32(is_chardev, pss.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_SET_STATEBLOB: 0x%x\n",
                    res);
            had_error = 1;
            goto cleanup;
        }

        while (true) {
            n = read(file_fd, buffer, buffersize);
            if (n < 0) {
                fprintf(stderr, "Could not read from file: %s\n",
                        strerror(errno));
                had_error = 1;
                goto cleanup;
            }
            if (n != write(fd, buffer, n)) {
                fprintf(stderr, "Could not write to file: %s\n",
                        strerror(errno));
                had_error = 1;
                goto cleanup;
            }

            if (!is_chardev) {
                n = read(fd, &pss.u.resp, sizeof(pss.u.resp));
                if (n < 0) {
                    fprintf(stderr,
                            "Error reading response: %s\n",
                            strerror(errno));
                    had_error = 1;
                    goto cleanup;
                }
                if (n != sizeof(pss.u.resp)) {
                    fprintf(stderr,
                            "Did not get enough response bytes "
                            "from PTM_SET_STATE_BLOB: %d\n", n);
                    had_error = 1;
                    goto cleanup;
                }
                res = devtoh32(is_chardev, pss.u.resp.tpm_result);
                if (res != 0) {
                    fprintf(stderr,
                            "TPM result from PTM_SET_STATEBLOB: 0x%x\n",
                            res);
                    had_error = 1;
                    goto cleanup;
                }
                break;
            }

            if ((size_t)n < buffersize) {
                /* close transfer with the ioctl() */
                pss.u.req.state_flags = htodev32(is_chardev, 0);
                pss.u.req.type = htodev32(is_chardev, bt);
                /* end the transfer */
                pss.u.req.length = htodev32(is_chardev, 0);

                n = ctrlcmd(fd, PTM_SET_STATEBLOB, &pss,
                            offsetof(ptm_setstate, u.req.data) + 0,
                            sizeof(pss.u.resp));
                if (n < 0) {
                    fprintf(stderr,
                            "Could not execute ioctl PTM_SET_STATEBLOB: "
                            "%s\n", strerror(errno));
                    had_error = 1;
                    goto cleanup;
                }
                res = devtoh32(is_chardev, pss.u.resp.tpm_result);
                if (res != 0) {
                    fprintf(stderr,
                            "TPM result from PTM_SET_STATEBLOB: 0x%x\n",
                            res);
                    had_error = 1;
                    goto cleanup;
                }
                break;
            }
        }
    }

 cleanup:
    close(file_fd);

    free(buffer);

    if (had_error)
        return 1;

    return 0;
}

static int change_fd_flags(int fd, int flags_to_clear, int flags_to_set) {
    int n;
    int orig_flags = fcntl(fd, F_GETFL, 0);

    if (orig_flags == -1) {
        fprintf(stderr, "fcntl(F_GETFL) failed: %s\n", strerror(errno));
        return -1;
    } else {
        int flags = (orig_flags & ~flags_to_clear) | flags_to_set;
        n = fcntl(fd, F_SETFL, flags);
        if (n == -1) {
            fprintf(stderr, "fcntl(F_SETFL) failed: %s\n", strerror(errno));
            return -1;
        }
    }
    return orig_flags;
}

/* Create a connection by setting the given file descriptor to non-blocking.
 * If the function returns successfully, the file descriptor will be blocking
 * again.
 */
static int connect_nonblock(int fd, const struct sockaddr *addr,
                            socklen_t addrlen, int allowed_errno,
                            const char *socktype)
{
    int n, sockerr;
    socklen_t optlen = sizeof(sockerr);
#if !defined(__CYGWIN__)
    int orig_flags = change_fd_flags(fd, 0, O_NONBLOCK);
#else
    int orig_flags = change_fd_flags(fd, 0, 0);
#endif

    n = connect(fd, addr, addrlen);
    /* n < 0: it must fail with EAGAIN (Unix socket) and then we have to poll
     *        and get SO_ERROR
     * n = 0: connection is established
     */
    if (n < 0 && errno != allowed_errno) {
        fprintf(stderr, "Connect failed: %s\n", strerror(errno));
        return -1;
    } else if (n < 0) {
        struct pollfd pollfd = {
            .fd = fd,
            .events = POLLOUT,
        };

        n = poll(&pollfd, 1, DEFAULT_POLL_TIMEOUT);
        /* Unix socket may return POLLHUP on error */
        if (n != 1 || (pollfd.revents & ~POLLOUT) != 0) {
            fprintf(stderr, "Could not connect using %s socket.\n", socktype);
            return -1;
        }

        n = getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockerr, &optlen);
        if (n < 0) {
            fprintf(stderr, "getsockopt(SO_ERROR) failed: %s\n",
                    strerror(errno));
            return -1;
        }
        if (sockerr != 0) {
            fprintf(stderr, "Could not connect using %s socket.\n", socktype);
            return -1;
        }
    }
    return change_fd_flags(fd, ~0, orig_flags);
}

static int open_connection(const char *devname, char *tcp_hostname,
                           unsigned short tcp_port, const char *unix_path)
{
    int fd = -1;

    if (devname) {
        fd = open(devname, O_RDWR);
        if (fd < 0) {
            fprintf(stderr, "Unable to open device '%s'.\n", devname);
        }
    } else if (tcp_hostname) {
        struct addrinfo hints = {
            .ai_family = AF_UNSPEC,
            .ai_socktype = SOCK_STREAM,
        };
        struct addrinfo *ais = NULL, *ai;
        char portstr[10];

        snprintf(portstr, sizeof(portstr), "%u", tcp_port);

        int err = getaddrinfo(tcp_hostname, portstr, &hints, &ais);
        if (err != 0) {
            fprintf(stderr, "getaddrinfo failed on host '%s': %s\n",
                    tcp_hostname, gai_strerror(err));
            return -1;
        }

        for (ai = ais; ai != NULL; ai = ai->ai_next) {
            fd = socket(ai->ai_family, ai->ai_socktype, 0);
            if (fd < 0)
                continue;

            if (connect(fd,
                        (struct sockaddr *)ai->ai_addr, ai->ai_addrlen) == 0)
                break;
            close(fd);
            fd = -1;
        }
        freeaddrinfo(ais);

        if (fd < 0) {
            fprintf(stderr, "Could not connect to host '%s' on port '%u' "
                    "using TCP socket: %s\n", tcp_hostname, tcp_port,
                    strerror(errno));
        }
    } else if (unix_path) {
        struct sockaddr_un addr;
        size_t unix_path_len = strlen(unix_path) + 1;

        if (unix_path_len > sizeof(addr.sun_path)) {
            fprintf(stderr, "Socket path is too long.\n");
            return -1;
        }

        fd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (fd > 0) {
            addr.sun_family = AF_UNIX;
            strncpy(addr.sun_path, unix_path, unix_path_len);
            if (connect_nonblock(fd, (struct sockaddr*)&addr,
                                 sizeof(addr), EAGAIN, "UnixIO") < 0) {
                close(fd);
                fd = -1;
            }
        }
    }

    return fd;
}

static int parse_tcp_optarg(char *optarg, char **tcp_hostname,
                            unsigned int *tcp_port)
{
    char *pos = strrchr(optarg, ':');
    int n;

    *tcp_port = DEFAULT_TCP_PORT;

    if (!pos) {
        /* <server> */
        *tcp_hostname = strdup(optarg);
        if (*tcp_hostname == NULL) {
            fprintf(stderr, "Out of memory.\n");
            return -1;
        }
        return 0;
    } else if (pos == optarg) {
        if (strlen(&pos[1]) != 0) {
            /* :<port>  (not just ':') */
            n = sscanf(&pos[1], "%u", tcp_port);
            if (n != 1) {
                fprintf(stderr, "Invalid port '%s'\n", &pos[1]);
                return -1;
            }
            if (*tcp_port >= 65536) {
                fprintf(stderr, "Port '%s' outside valid range.\n",
                    &optarg[1]);
                return -1;
            }
        }

        *tcp_hostname = strdup("127.0.0.1");
        if (*tcp_hostname == NULL) {
            fprintf(stderr, "Out of memory.\n");
            return -1;
        }
    } else {
        /* <server>:<port> */
        n = sscanf(&pos[1], "%u", tcp_port);
        if (n != 1) {
            fprintf(stderr, "Invalid port '%s'\n", &pos[1]);
            return -1;
        }
        if (*tcp_port >= 65536) {
            fprintf(stderr, "Port '%s' outside valid range.\n",
                &optarg[1]);
            return -1;
        }

        *tcp_hostname = strndup(optarg, pos - optarg);
        if (*tcp_hostname == NULL) {
            fprintf(stderr, "Out of memory.\n");
            return -1;
        }
    }
    return 0;
}

static void versioninfo(void)
{
    fprintf(stdout,
"TPM emulator control tool version %d.%d.%d, Copyright (c) 2015 IBM Corp.\n"
,SWTPM_VER_MAJOR, SWTPM_VER_MINOR, SWTPM_VER_MICRO);
}

static void usage(const char *prgname)
{
    versioninfo();
    fprintf(stdout,
"\n"
"Usage: %s command <device path>\n"
"\n"
"The following commands are supported:\n"
"--tpm-device <device> : use the given device; default is /dev/tpm0\n"
"--tcp [<host>]:[<prt>]: connect to TPM on given host and port;\n"
"                        default host is 127.0.0.1, default port is %u\n"
"--unix <path>         : connect to TPM using UnixIO socket\n"
"-c                    : get ptm capabilities\n"
"-i                    : do a hardware TPM_Init; if volatile state is found,\n"
"                        it will resume the TPM with it and delete it\n"
"                        afterwards\n"
"--stop                : stop the TPM without exiting\n"
"-s                    : shutdown the TPM; stops and exists\n"
"-e                    : get the tpmEstablished bit\n"
"-r <loc>              : reset the tpmEstablished bit; use the given locality\n"
"-v                    : store the TPM's volatile data\n"
"-C                    : cancel an ongoing TPM command\n"
"-l <loc>              : set the locality to the given number; valid\n"
"                        localities are 0-4\n"
"-h <data>             : hash the given data; if data is '-' then data are\n"
"                        read from stdin\n"
"--save <type> <file>  : store the TPM state blob of given type in a file;\n"
"                        type may be one of volatile, permanent, or savestate\n"
"--load <type> <file>  : load the TPM state blob of given type from a file;\n"
"                        type may be one of volatile, permanent, or savestate\n"
"-g                    : get configuration flags indicating which keys are in\n"
"                        use\n"
"-b <buffersize>       : set the buffer size of the TPM and get its current\n"
"                        size; get minimum and maximum supported sizes\n"
"--info <flags>        : get TPM implementation specific information;\n"
"                        flags must be an integer value\n"
"--lock-storage <n>    : lock the storage after it was unlocked; retry\n"
"                        n times with 10ms delay in between\n"
"--version             : display version and exit\n"
"--help                : display help screen and exit\n"
"\n"
, prgname, DEFAULT_TCP_PORT);
}

int main(int argc, char *argv[])
{
    int fd = -1, n;
    ptm_est est;
    ptm_reset_est reset_est;
    ptm_loc loc;
    ptm_cap cap;
    ptm_res res;
    ptm_init init;
    ptm_getconfig cfg;
    ptm_setbuffersize psbs;
    ptm_getinfo pgi;
    ptm_lockstorage pls;
    char *tmp;
    size_t buffersize = 0;
    static struct option long_options[] = {
        {"tpm-device", required_argument, NULL, 'D'},
        {"tcp", required_argument, NULL, 'T'},
        {"unix", required_argument, NULL, 'U'},
        {"c", no_argument, NULL, 'c'},
        {"i", no_argument, NULL, 'i'},
        {"stop", no_argument, NULL, 't'},
        {"s", no_argument, NULL, 's'},
        {"e", no_argument, NULL, 'e'},
        {"r", required_argument, NULL, 'r'},
        {"v", no_argument, NULL, 'v'},
        {"C", no_argument, NULL, 'C'},
        {"l", required_argument, NULL, 'l'},
        {"h", required_argument, NULL, 'h'},
        {"g", no_argument, NULL, 'g'},
        {"b", required_argument, NULL, 'b'},
        {"save", required_argument, NULL, 'S'},
        {"load", required_argument, NULL, 'L'},
        {"version", no_argument, NULL, 'V'},
        {"info", required_argument, NULL, 'I'},
        {"lock-storage", required_argument, NULL, 'o'},
        {"help", no_argument, NULL, 'H'},
        {NULL, 0, NULL, 0},
    };
    int opt, option_index = 0;
    const char *command = NULL, *pcommand = NULL;
    const char *blobtype = NULL, *blobfile = NULL, *hashdata = NULL;
    const char *tpm_device = NULL, *unix_path = NULL;
    char *tcp_hostname = NULL;
    unsigned int locality = 0;
    unsigned int tpmbuffersize = 0;
    unsigned int tcp_port = 0;
    unsigned int retries;
    bool is_chardev;
    unsigned long int info_flags = 0;
    char *endptr = NULL;
    int ret = EXIT_FAILURE;

#if defined __NetBSD__
    while ((opt = getopt_long(argc, argv, "D:T:U:citser:vCl:h:gb:S:L:VI:H",
                              long_options, &option_index)) != -1) {
#else
    while ((opt = getopt_long_only(argc, argv, "", long_options,
                                   &option_index)) != -1) {
#endif
        switch (opt) {
        case 'D':
            tpm_device = optarg;
            break;
        case 'T':
            free(tcp_hostname);
            if (parse_tcp_optarg(optarg, &tcp_hostname, &tcp_port) < 0)
                return EXIT_FAILURE;
            break;
        case 'U':
            unix_path = optarg;
            break;
        case 'c':
        case 'i':
        case 't':
        case 's':
        case 'e':
        case 'v':
        case 'C':
        case 'g':
            command = argv[optind - 1];
            break;
        case 'h':
            command = argv[optind - 2];
            hashdata = argv[optind - 1];
            break;
        case 'r':
        case 'l':
            command = argv[optind - 2];
            if (sscanf(argv[optind - 1], "%u", &locality) != 1) {
                fprintf(stderr, "Could not get locality number from %s.\n",
                        argv[optind - 1]);
                goto exit;
            }
            if (locality > 4) {
                fprintf(stderr, "Locality outside valid range of [0..4].\n");
                goto exit;
            }
            break;
        case 'S':
            if (optind == argc ||
                !strncmp(argv[optind], "-", 1) ||
                !strncmp(argv[optind], "--", 2)) {
                fprintf(stderr, "Missing filename argument for --save option\n");
                goto exit;
            }
            command = argv[optind - 2];
            blobtype = argv[optind - 1];
            blobfile = argv[optind];
            optind++;
            break;
        case 'L':
            if (optind == argc ||
                !strncmp(argv[optind], "-", 1) ||
                !strncmp(argv[optind], "--", 2)) {
                fprintf(stderr, "Missing filename argument for --load option\n");
                goto exit;
            }
            command = argv[optind - 2];
            blobtype = argv[optind - 1];
            blobfile = argv[optind];
            optind++;
            break;
        case 'b':
            command = argv[optind - 2];
            if (sscanf(argv[optind - 1], "%u", &tpmbuffersize) != 1) {
                fprintf(stderr, "Could not get buffersize from %s.\n",
                        argv[optind - 1]);
                goto exit;
            }
            break;
        case 'I':
            command = argv[optind - 2];
            errno = 0;
            info_flags = strtoul(argv[optind - 1], &endptr, 0);
            if (errno || endptr[0] != '\0') {
                fprintf(stderr, "Cannot parse info flags.\n");
                goto exit;
            }
            break;
        case 'o':
            command = argv[optind - 2];
            if (sscanf(argv[optind - 1], "%u", &retries) != 1) {
                fprintf(stderr, "Could not parse lock retries from %s.\n",
                        argv[optind - 1]);
                goto exit;
            }
            break;
        case 'V':
            versioninfo();
            ret = EXIT_SUCCESS;
            goto exit;
        case 'H':
            usage(argv[0]);
            ret = EXIT_SUCCESS;
            goto exit;
        }
        if (!pcommand) {
            pcommand = command;
        } else {
            if (command != pcommand) {
                fprintf(stderr, "Only one command may be given.\n");
                goto exit;
            }
        }
    }

    if (!command) {
        fprintf(stderr, "No valid command.\n");
        goto exit;
    }

    if (!tpm_device && !tcp_hostname && !unix_path) {
        if (optind == argc) {
            fprintf(stderr, "Error: Missing device name.\n");
            goto exit;
        }

        tpm_device = argv[optind];
    }

    is_chardev = (tpm_device != NULL);
    if (is_chardev) {
        tmp = getenv("SWTPM_IOCTL_BUFFERSIZE");
        if (tmp) {
            if (sscanf(tmp, "%zu", &buffersize) != 1 || buffersize < 1)
                buffersize = 1;
        }
    } else {
        if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
            fprintf(stderr, "Could not install signal handler for SIGPIPE.");
            goto exit;
        }
    }

    fd = open_connection(tpm_device, tcp_hostname, tcp_port, unix_path);
    if (fd < 0) {
        goto exit;
    }

    if (!strcmp(command, "-c")) {
        n = ctrlcmd(fd, PTM_GET_CAPABILITY, &cap, 0, sizeof(cap));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_GET_CAPABILITY: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        /* no tpm_result here */
        printf("ptm capability is 0x%" PRIx64 "\n",
               (uint64_t)devtoh64(is_chardev, cap));

    } else if (!strcmp(command, "-i")) {
        init.u.req.init_flags = htodev32(is_chardev,
                                         PTM_INIT_FLAG_DELETE_VOLATILE);
        n = ctrlcmd(fd, PTM_INIT, &init, sizeof(init.u.req),
                    sizeof(init.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_INIT: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, init.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_INIT: 0x%x\n", res);
            goto exit;
        }

    } else if (!strcmp(command, "-e")) {
        n = ctrlcmd(fd, PTM_GET_TPMESTABLISHED, &est, 0, sizeof(est.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_GET_ESTABLISHED: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, est.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_GET_TPMESTABLISHED: 0x%x\n", res);
            goto exit;
        }
        printf("tpmEstablished is %d\n", est.u.resp.bit);

    } else if (!strcmp(command, "-r")) {
        memset(&reset_est, 0, sizeof(reset_est));
        reset_est.u.req.loc = locality;
        n = ctrlcmd(fd, PTM_RESET_TPMESTABLISHED,
                    &reset_est, sizeof(reset_est.u.req),
                    sizeof(reset_est.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_RESET_ESTABLISHED: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, reset_est.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_RESET_TPMESTABLISHED: 0x%x\n", res);
            goto exit;
        }

    } else if (!strcmp(command, "-s")) {
        n = ctrlcmd(fd, PTM_SHUTDOWN, &res, 0, sizeof(res));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_SHUTDOWN: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        if (devtoh32(is_chardev, res) != 0) {
            fprintf(stderr,
                    "TPM result from PTM_SHUTDOWN: 0x%x\n",
                    devtoh32(is_chardev, res));
            goto exit;
        }

    } else if (!strcmp(command, "--stop")) {
        n = ctrlcmd(fd, PTM_STOP, &res, 0, sizeof(res));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_STOP: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        if (devtoh32(is_chardev, res) != 0) {
            fprintf(stderr,
                    "TPM result from PTM_STOP: 0x%x\n",
                    devtoh32(is_chardev, res));
            goto exit;
        }

    } else if (!strcmp(command, "-l")) {
        loc.u.req.loc = locality;
        n = ctrlcmd(fd, PTM_SET_LOCALITY, &loc, sizeof(loc.u.req),
                    sizeof(loc.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_SET_LOCALITY: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, loc.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_SET_LOCALITY: 0x%x\n", res);
            goto exit;
        }

    } else if (!strcmp(command, "-h")) {
        if (do_hash_start_data_end(fd, is_chardev, hashdata)) {
            goto exit;
        }

    } else if (!strcmp(command, "-C")) {
        n = ctrlcmd(fd, PTM_CANCEL_TPM_CMD, &res, 0, sizeof(res));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_CANCEL_TPM_CMD: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        if (devtoh32(is_chardev, res) != 0) {
            fprintf(stderr,
                    "TPM result from PTM_CANCEL_TPM_CMD: 0x%x\n",
                    devtoh32(is_chardev, res));
            goto exit;
        }

    } else if (!strcmp(command, "-v")) {
        n = ctrlcmd(fd, PTM_STORE_VOLATILE, &res, 0, sizeof(res));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_STORE_VOLATILE: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        if (devtoh32(is_chardev, res) != 0) {
            fprintf(stderr,
                    "TPM result from PTM_STORE_VOLATILE: 0x%x\n",
                    devtoh32(is_chardev, res));
            goto exit;
        }

    } else if (!strcmp(command, "--save")) {
        if (do_save_state_blob(fd, is_chardev, blobtype, blobfile, buffersize))
            goto exit;

    } else if (!strcmp(command, "--load")) {
        if (do_load_state_blob(fd, is_chardev, blobtype, blobfile, buffersize))
            goto exit;

    } else if (!strcmp(command, "-g")) {
        n = ctrlcmd(fd, PTM_GET_CONFIG, &cfg, 0, sizeof(cfg));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_GET_CONFIG: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, cfg.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_GET_CONFIG: 0x%x\n", res);
            goto exit;
        }
        printf("ptm configuration flags: 0x%x\n",
               devtoh32(is_chardev, cfg.u.resp.flags));
    } else if (!strcmp(command, "-b")) {
        memset(&psbs, 0, sizeof(psbs));
        psbs.u.req.buffersize = htodev32(is_chardev, tpmbuffersize);
        n = ctrlcmd(fd, PTM_SET_BUFFERSIZE, &psbs, sizeof(psbs.u.req),
                    sizeof(psbs.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_SET_BUFFERSIZE: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, psbs.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_SET_BUFFERSIZE: 0x%x\n", res);
            goto exit;
        }
        printf("TPM buffersize: %u\n"
               "minimum size  : %u\n"
               "maximum size  : %u\n",
               devtoh32(is_chardev, psbs.u.resp.buffersize),
               devtoh32(is_chardev, psbs.u.resp.minsize),
               devtoh32(is_chardev, psbs.u.resp.maxsize));
    } else if (!strcmp(command, "--info")) {
        memset(&pgi, 0, sizeof(pgi));
        pgi.u.req.flags = htodev64(is_chardev, info_flags);
        pgi.u.req.offset = htodev64(is_chardev, 0);
        n = ctrlcmd(fd, PTM_GET_INFO, &pgi,
                    sizeof(pgi.u.req), sizeof(pgi.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_GET_INFO: %s\n",
                    strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, pgi.u.resp.tpm_result);
        if (res != 0) {
            fprintf(stderr,
                    "TPM result from PTM_GET_INFO: 0x%x\n", res);
            goto exit;
        }
        printf("%s\n", pgi.u.resp.buffer);
    } else if (!strcmp(command, "--lock-storage")) {
        memset(&pls, 0, sizeof(pls));
        pls.u.req.retries = htodev32(is_chardev, 0);

        n = ctrlcmd(fd, PTM_LOCK_STORAGE, &pls,
                    sizeof(pls.u.req), sizeof(pls.u.resp));
        if (n < 0) {
            fprintf(stderr,
                    "Could not execute PTM_LOCK_STORAGE: "
                    "%s\n", strerror(errno));
            goto exit;
        }
        res = devtoh32(is_chardev, pls.u.resp.tpm_result);
        if (devtoh32(is_chardev, res) != 0) {
            fprintf(stderr,
                    "TPM result from PTM_LOCK_STORAGE: 0x%x\n",
                    devtoh32(is_chardev, res));
            goto exit;
        }

    } else {
        usage(argv[0]);
        goto exit;
    }

    ret = EXIT_SUCCESS;

exit:
    free(tcp_hostname);
    if (fd >= 0)
        close(fd);

    return ret;
}
