/*
 * Server side of the old-school SCP protocol.
 */

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

#include "putty.h"
#include "ssh.h"
#include "sshcr.h"
#include "channel.h"
#include "sftp.h"

/*
 * I think it's worth actually documenting my understanding of what
 * this protocol _is_, since I don't know of any other documentation
 * of it anywhere.
 *
 * Format of data stream
 * ---------------------
 *
 * The sending side of an SCP connection - the client, if you're
 * uploading files, or the server if you're downloading - sends a data
 * stream consisting of a sequence of 'commands', or header records,
 * or whatever you want to call them, interleaved with file data.
 *
 * Each command starts with a letter indicating what type it is, and
 * ends with a \n.
 *
 * The 'C' command introduces an actual file. It is followed by an
 * octal file-permissions mask, then a space, then a decimal file
 * size, then a space, then the file name up to the termating newline.
 * For example, "C0644 12345 filename.txt\n" would be a plausible C
 * command.
 *
 * After the 'C' command, the sending side will transmit exactly as
 * many bytes of file data as specified by the size field in the
 * header line, followed by a single zero byte.
 *
 * The 'D' command introduces a subdirectory. Its format is identical
 * to 'C', including the size field, but the size field is sent as
 * zero.
 *
 * After the 'D' command, all subsequent C and D commands are taken to
 * indicate files that should be placed inside that subdirectory,
 * until a terminating 'E' command.
 *
 * The 'E' command indicates the end of a subdirectory. It has no
 * arguments at all (its format is always just "E\n"). After the E
 * command, the receiver should revert to placing further downloaded
 * files in whatever directory it was placing them before the
 * subdirectory opened by the just-closed D.
 *
 * D and E commands match like parentheses: if you send, say,
 *
 *    C0644 123 foo.txt       ( followed by data )
 *    D0755 0 subdir
 *    C0644 123 bar.txt       ( followed by data )
 *    D0755 0 subsubdir
 *    C0644 123 baz.txt       ( followed by data )
 *    E
 *    C0644 123 quux.txt      ( followed by data )
 *    E
 *    C0644 123 wibble.txt    ( followed by data )
 *
 * then foo.txt, subdir and wibble.txt go in the top-level destination
 * directory; bar.txt, subsubdir and quux.txt go in 'subdir'; and
 * baz.txt goes in 'subdir/subsubdir'.
 *
 * The sender terminates the data stream with EOF when it has no more
 * files to send. I believe it is not _required_ for all D to be
 * closed by an E before this happens - you can elide a trailing
 * sequence of E commands without provoking an error message from the
 * receiver.
 *
 * Finally, the 'T' command is sent immediately before a C or D. It is
 * followed by four space-separated decimal integers giving an mtime
 * and atime to be applied to the file or directory created by the
 * following C or D command. The first two integers give the mtime,
 * encoded as seconds and microseconds (respectively) since the Unix
 * epoch; the next two give the atime, encoded similarly. So
 * "T1540373455 0 1540373457 0\n" is an example of a valid T command.
 *
 * Acknowledgments
 * ---------------
 *
 * The sending side waits for an ack from the receiving side before
 * sending each command; before beginning to send the file data
 * following a C command; and before sending the final EOF.
 *
 * (In particular, the receiving side is expected to send an initial
 * ack before _anything_ is sent.)
 *
 * Normally an ack consists of a single zero byte. It's also allowable
 * to send a byte with value 1 or 2 followed by a \n-terminated error
 * message (where 1 means a non-fatal error and 2 means a fatal one).
 * I have to suppose that sending an error message from client to
 * server is of limited use, but apparently it's allowed.
 *
 * Initiation
 * ----------
 *
 * The protocol is begun by the client sending a command string to the
 * server via the SSH-2 "exec" request (or the analogous
 * SSH1_CMSG_EXEC_CMD), which indicates that this is an scp session
 * rather than any other thing; specifies the direction of transfer;
 * says what file(s) are to be sent by the server, or where the server
 * should put files that the client is about to send; and a couple of
 * other options.
 *
 * The command string takes the following form:
 *
 * Start with prefix "scp ", indicating that this is an SCP command at
 * all. Otherwise it's a request to run some completely different
 * command in the SSH session.
 *
 * Next the command can contain zero or more of the following options,
 * each followed by a space:
 *
 * "-v" turns on verbose server diagnostics. Of course a server is not
 * required to actually produce any, but this is an invitation for it
 * to send any it might have available. Diagnostics are free-form, and
 * sent as SSH standard-error extended data, so that they are separate
 * from the actual data stream as described above.
 *
 * (Servers can send standard-error output anyway if they like, and in
 * case of an actual error, they probably will with or without -v.)
 *
 * "-r" indicates recursive file transfer, i.e. potentially including
 * subdirectories. For a download, this indicates that the client is
 * willing to receive subdirectories (a D/E command pair bracketing
 * further files and subdirs); without it, the server should only send
 * C commands for individual files, followed by EOF.
 *
 * This flag must also be specified for a recursive upload, because I
 * believe at least one server will reject D/E pairs sent by the
 * client if the command didn't have -r in it. (Probably a consequence
 * of sharing code between download and upload.)
 *
 * "-p" means preserve file times. In a download, this requests the
 * server to send a T command before each C or D. I don't know whether
 * any server will insist on having seen this option from the client
 * before accepting T commands in an upload, but it is probably
 * sensible to send it anyway.
 *
 * "-d", in an upload, means that the destination pathname (see below)
 * is expected to be a directory, and that uploaded files (and
 * subdirs) should be put inside it. Without -d, the semantics are
 * that _if_ the destination exists and is a directory, then files
 * will be put in it, whereas if it is not, then just a single file
 * (or subdir) upload is expected, which will be placed at that exact
 * pathname.
 *
 * In a download, I observe that clients tend to send -d if they are
 * requesting multiple files or a wildcard, but as far as I know,
 * servers ignore it.
 *
 * After all those optional options, there is a single mandatory
 * option indicating the direction of transfer, which is either "-f"
 * or "-t". "-f" indicates a download; "-t" indicates an upload.
 *
 * After that mandatory option, there is a single space, followed by
 * the name(s) of files to transfer.
 *
 * This file name field is transmitted with NO QUOTING, in spite of
 * the fact that a server will typically interpret it as a shell
 * command. You'd think this couldn't possibly work, in the face of
 * almost any filename with an interesting character in it - and you'd
 * be right. Or rather (you might argue), it works 'as designed', but
 * it's designed in a weird way, in that it's the user's
 * responsibility to apply quoting on the client command line to get
 * the filename through the shell that will decode things on the
 * server side.
 *
 * But one effect of this is that if you issue a download command
 * including a wildcard, say "scp -f somedir/foo*.txt", then the shell
 * will expand the wildcard, and actually run the server-side scp
 * program with multiple arguments, say "somedir/foo.txt
 * somedir/quux.txt", leading to the download sending multiple C
 * commands. This clearly _is_ intended: it's how a command such as
 * 'scp server:somedir/foo*.txt destdir' can work at all.
 *
 * (You would think, given that, that it might also be legal to send
 * multiple space-separated filenames in order to trigger a download
 * of exactly those files. Given how scp is invoked in practice on a
 * typical server, this would surely actually work, but my observation
 * is that scp clients don't in fact try this - if you run OpenSSH's
 * scp by saying 'scp server:foo server:bar destdir' then it will make
 * two separate connections to the server for the two files, rather
 * than sending a single space-separated remote command. PSCP won't
 * even do that, and will make you do it in two separate runs.)
 *
 * So, some examples:
 *
 *  - "scp -f filename.txt"
 *
 *    Server should send a single C command (plus data) for that file.
 *    Client ought to ignore the filename in the C command, in favour
 *    of saving the file under the name implied by the user's command
 *    line.
 *
 *  - "scp -f file*.txt"
 *
 *    Server sends zero or more C commands, then EOF. Client will have
 *    been given a target directory to put them all in, and will name
 *    each one according to the name in the C command.
 *
 *    (You'd like the client to validate the filenames against the
 *    wildcard it sent, to ensure a malicious server didn't try to
 *    overwrite some path like ".bashrc" when you thought you were
 *    downloading only normal text files. But wildcard semantics are
 *    chosen by the server, so this is essentially hopeless to do
 *    rigorously.)
 *
 *  - "scp -f -r somedir"
 *
 *    Assuming somedir is actually a directory, server sends a D/E
 *    pair, in between which are the contents of the directory
 *    (perhaps including further nested D/E pairs). Client probably
 *    ignores the name field of the outermost D
 *
 *  - "scp -f -r some*wild*card*"
 *
 *    Server sends multiple C or D-stuff-E, one for each top-level
 *    thing matching the wildcard, whether it's a file or a directory.
 *
 *  - "scp -t -d some_dir"
 *
 *    Client sends stuff, and server deposits each file at
 *    some_dir/<name from the C command>.
 *
 *  - "scp -t some_path_name"
 *
 *    Client sends one C command, and server deposits it at
 *    some_path_name itself, or in some_path_name/<name from C
 *    command>, depending whether some_path_name was already a
 *    directory or not.
 */

/*
 * Here's a useful debugging aid: run over a binary file containing
 * the complete contents of the sender's data stream (e.g. extracted
 * by contrib/logparse.pl -d), it removes the file contents, leaving
 * only the list of commands, so you can see what the server sent.
 *
 *   perl -pe 'read ARGV,$x,1+$1 if/^C\S+ (\d+)/'
 */

/* ----------------------------------------------------------------------
 * Shared system for receiving replies from the SftpServer, and
 * putting them into a set of ordinary variables rather than
 * marshalling them into actual SFTP reply packets that we'd only have
 * to unmarshal again.
 */

typedef struct ScpReplyReceiver ScpReplyReceiver;
struct ScpReplyReceiver {
    bool err;
    unsigned code;
    char *errmsg;
    struct fxp_attrs attrs;
    ptrlen name, handle, data;

    SftpReplyBuilder srb;
};

static void scp_reply_ok(SftpReplyBuilder *srb)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    reply->err = false;
}

static void scp_reply_error(
    SftpReplyBuilder *srb, unsigned code, const char *msg)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    reply->err = true;
    reply->code = code;
    sfree(reply->errmsg);
    reply->errmsg = dupstr(msg);
}

static void scp_reply_name_count(SftpReplyBuilder *srb, unsigned count)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    reply->err = false;
}

static void scp_reply_full_name(
    SftpReplyBuilder *srb, ptrlen name,
    ptrlen longname, struct fxp_attrs attrs)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    char *p;
    reply->err = false;
    sfree((void *)reply->name.ptr);
    reply->name.ptr = p = mkstr(name);
    reply->name.len = name.len;
    reply->attrs = attrs;
}

static void scp_reply_simple_name(SftpReplyBuilder *srb, ptrlen name)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    reply->err = false;
}

static void scp_reply_handle(SftpReplyBuilder *srb, ptrlen handle)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    char *p;
    reply->err = false;
    sfree((void *)reply->handle.ptr);
    reply->handle.ptr = p = mkstr(handle);
    reply->handle.len = handle.len;
}

static void scp_reply_data(SftpReplyBuilder *srb, ptrlen data)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    char *p;
    reply->err = false;
    sfree((void *)reply->data.ptr);
    reply->data.ptr = p = mkstr(data);
    reply->data.len = data.len;
}

static void scp_reply_attrs(
    SftpReplyBuilder *srb, struct fxp_attrs attrs)
{
    ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
    reply->err = false;
    reply->attrs = attrs;
}

static const SftpReplyBuilderVtable ScpReplyReceiver_vt = {
    .reply_ok = scp_reply_ok,
    .reply_error = scp_reply_error,
    .reply_simple_name = scp_reply_simple_name,
    .reply_name_count = scp_reply_name_count,
    .reply_full_name = scp_reply_full_name,
    .reply_handle = scp_reply_handle,
    .reply_data = scp_reply_data,
    .reply_attrs = scp_reply_attrs,
};

static void scp_reply_setup(ScpReplyReceiver *reply)
{
    memset(reply, 0, sizeof(*reply));
    reply->srb.vt = &ScpReplyReceiver_vt;
}

static void scp_reply_cleanup(ScpReplyReceiver *reply)
{
    sfree(reply->errmsg);
    sfree((void *)reply->name.ptr);
    sfree((void *)reply->handle.ptr);
    sfree((void *)reply->data.ptr);
}

/* ----------------------------------------------------------------------
 * Source end of the SCP protocol.
 */

#define SCP_MAX_BACKLOG 65536

typedef struct ScpSource ScpSource;
typedef struct ScpSourceStackEntry ScpSourceStackEntry;

struct ScpSource {
    SftpServer *sf;

    int acks;
    bool expect_newline, eof, throttled, finished;

    SshChannel *sc;
    ScpSourceStackEntry *head;
    bool recursive;
    bool send_file_times;

    strbuf *pending_commands[3];
    int n_pending_commands;

    uint64_t file_offset, file_size;

    ScpReplyReceiver reply;

    ScpServer scpserver;
};

typedef enum ScpSourceNodeType ScpSourceNodeType;
enum ScpSourceNodeType { SCP_ROOTPATH, SCP_NAME, SCP_READDIR, SCP_READFILE };

struct ScpSourceStackEntry {
    ScpSourceStackEntry *next;
    ScpSourceNodeType type;
    ptrlen pathname, handle;
    const char *wildcard;
    struct fxp_attrs attrs;
};

static void scp_source_push(ScpSource *scp, ScpSourceNodeType type,
                            ptrlen pathname, ptrlen handle,
                            const struct fxp_attrs *attrs, const char *wc)
{
    size_t wc_len = wc ? strlen(wc)+1 : 0;
    ScpSourceStackEntry *node = snew_plus(
        ScpSourceStackEntry, pathname.len + handle.len + wc_len);
    char *namebuf = snew_plus_get_aux(node);
    memcpy(namebuf, pathname.ptr, pathname.len);
    node->pathname = make_ptrlen(namebuf, pathname.len);
    memcpy(namebuf + pathname.len, handle.ptr, handle.len);
    node->handle = make_ptrlen(namebuf + pathname.len, handle.len);
    if (wc) {
        strcpy(namebuf + pathname.len + handle.len, wc);
        node->wildcard = namebuf + pathname.len + handle.len;
    } else {
        node->wildcard = NULL;
    }
    node->attrs = attrs ? *attrs : no_attrs;
    node->type = type;
    node->next = scp->head;
    scp->head = node;
}

static char *scp_source_err_base(ScpSource *scp, const char *fmt, va_list ap)
{
    char *msg = dupvprintf(fmt, ap);
    sshfwd_write_ext(scp->sc, true, msg, strlen(msg));
    sshfwd_write_ext(scp->sc, true, "\012", 1);
    return msg;
}
static PRINTF_LIKE(2, 3) void scp_source_err(
    ScpSource *scp, const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    sfree(scp_source_err_base(scp, fmt, ap));
    va_end(ap);
}
static PRINTF_LIKE(2, 3) void scp_source_abort(
    ScpSource *scp, const char *fmt, ...)
{
    va_list ap;
    char *msg;

    va_start(ap, fmt);
    msg = scp_source_err_base(scp, fmt, ap);
    va_end(ap);

    sshfwd_send_exit_status(scp->sc, 1);
    sshfwd_write_eof(scp->sc);
    sshfwd_initiate_close(scp->sc, msg);

    scp->finished = true;
}

static void scp_source_push_name(
    ScpSource *scp, ptrlen pathname, struct fxp_attrs attrs, const char *wc)
{
    if (!(attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS)) {
        scp_source_err(scp, "unable to read file permissions for %.*s",
                       PTRLEN_PRINTF(pathname));
        return;
    }
    if (attrs.permissions & PERMS_DIRECTORY) {
        if (!scp->recursive && !wc) {
            scp_source_err(scp, "%.*s: is a directory",
                           PTRLEN_PRINTF(pathname));
            return;
        }
    } else {
        if (!(attrs.flags & SSH_FILEXFER_ATTR_SIZE)) {
            scp_source_err(scp, "unable to read file size for %.*s",
                           PTRLEN_PRINTF(pathname));
            return;
        }
    }

    scp_source_push(scp, SCP_NAME, pathname, PTRLEN_LITERAL(""), &attrs, wc);
}

static void scp_source_free(ScpServer *s);
static size_t scp_source_send(ScpServer *s, const void *data, size_t length);
static void scp_source_eof(ScpServer *s);
static void scp_source_throttle(ScpServer *s, bool throttled);

static const ScpServerVtable ScpSource_ScpServer_vt = {
    .free = scp_source_free,
    .send = scp_source_send,
    .throttle = scp_source_throttle,
    .eof = scp_source_eof,
};

static ScpSource *scp_source_new(
    SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen pathname)
{
    ScpSource *scp = snew(ScpSource);
    memset(scp, 0, sizeof(*scp));

    scp->scpserver.vt = &ScpSource_ScpServer_vt;
    scp_reply_setup(&scp->reply);
    scp->sc = sc;
    scp->sf = sftpsrv_new(sftpserver_vt);
    scp->n_pending_commands = 0;

    scp_source_push(scp, SCP_ROOTPATH, pathname, PTRLEN_LITERAL(""),
                    NULL, NULL);

    return scp;
}

static void scp_source_free(ScpServer *s)
{
    ScpSource *scp = container_of(s, ScpSource, scpserver);
    scp_reply_cleanup(&scp->reply);
    while (scp->n_pending_commands > 0)
        strbuf_free(scp->pending_commands[--scp->n_pending_commands]);
    while (scp->head) {
        ScpSourceStackEntry *node = scp->head;
        scp->head = node->next;
        sfree(node);
    }

    delete_callbacks_for_context(scp);

    sfree(scp);
}

static void scp_source_send_E(ScpSource *scp)
{
    strbuf *cmd;

    assert(scp->n_pending_commands == 0);

    scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
    put_fmt(cmd, "E\012");
}

static void scp_source_send_CD(
    ScpSource *scp, char cmdchar,
    struct fxp_attrs attrs, uint64_t size, ptrlen name)
{
    strbuf *cmd;

    assert(scp->n_pending_commands == 0);

    if (scp->send_file_times && (attrs.flags & SSH_FILEXFER_ATTR_ACMODTIME)) {
        scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
        /* Our SFTP-based filesystem API doesn't support microsecond times */
        put_fmt(cmd, "T%lu 0 %lu 0\012", attrs.mtime, attrs.atime);
    }

    const char *slash;
    while ((slash = memchr(name.ptr, '/', name.len)) != NULL)
        name = make_ptrlen(
            slash+1, name.len - (slash+1 - (const char *)name.ptr));

    scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
    put_fmt(cmd, "%c%04o %"PRIu64" %.*s\012", cmdchar,
            (unsigned)(attrs.permissions & 07777),
            size, PTRLEN_PRINTF(name));

    if (cmdchar == 'C') {
        /* We'll also wait for an ack before sending the file data,
         * which we record by saving a zero-length 'command' to be
         * sent after the C. */
        scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
    }
}

static void scp_source_process_stack(ScpSource *scp);
static void scp_source_process_stack_cb(void *vscp)
{
    ScpSource *scp = (ScpSource *)vscp;
    if (scp->finished)
        return;                        /* this callback is out of date */
    scp_source_process_stack(scp);
}
static void scp_requeue(ScpSource *scp)
{
    queue_toplevel_callback(scp_source_process_stack_cb, scp);
}

static void scp_source_process_stack(ScpSource *scp)
{
    if (scp->throttled)
        return;

    while (scp->n_pending_commands > 0) {
        /* Expect an ack, and consume it */
        if (scp->eof) {
            scp_source_abort(
                scp, "scp: received client EOF, abandoning transfer");
            return;
        }
        if (scp->acks == 0)
            return;
        scp->acks--;

        /*
         * Now send the actual command (unless it was the phony
         * zero-length one that indicates our need for an ack before
         * beginning to send file data).
         */

        if (scp->pending_commands[0]->len)
            sshfwd_write(scp->sc, scp->pending_commands[0]->s,
                         scp->pending_commands[0]->len);

        strbuf_free(scp->pending_commands[0]);
        scp->n_pending_commands--;
        if (scp->n_pending_commands > 0) {
            /*
             * We still have at least one pending command to send, so
             * move up the queue.
             *
             * (We do that with a bodgy memmove, because there are at
             * most a bounded number of commands ever pending at once,
             * so no need to worry about quadratic time.)
             */
            memmove(scp->pending_commands, scp->pending_commands+1,
                    scp->n_pending_commands * sizeof(*scp->pending_commands));
        }
    }

    /*
     * Mostly, we start by waiting for an ack byte from the receiver.
     */
    if (scp->head && scp->head->type == SCP_READFILE && scp->file_offset) {
        /*
         * Exception: if we're already in the middle of transferring a
         * file, we'll be called back here because the channel backlog
         * has cleared; we don't need to wait for an ack.
         */
    } else if (scp->head && scp->head->type == SCP_ROOTPATH) {
        /*
         * Another exception: the initial action node that makes us
         * stat the root path. We'll translate it into an SCP_NAME,
         * and _that_ will require an ack.
         */
        ScpSourceStackEntry *node = scp->head;
        scp->head = node->next;

        /*
         * Start by checking if there's a wildcard involved in the
         * root path.
         */
        char *rootpath_str = mkstr(node->pathname);
        char *rootpath_unesc = snewn(1+node->pathname.len, char);
        ptrlen pathname;
        const char *wildcard;

        if (wc_unescape(rootpath_unesc, rootpath_str)) {
            /*
             * We successfully removed instances of the escape
             * character used in our wildcard syntax, without
             * encountering any actual wildcard chars - i.e. this is
             * not a wildcard, just a single file. The simple case.
             */
            pathname = ptrlen_from_asciz(rootpath_str);
            wildcard = NULL;
        } else {
            /*
             * This is a wildcard. Separate it into a directory name
             * (which we enforce mustn't contain wc characters, for
             * simplicity) and a wildcard to match leaf names.
             */
            char *last_slash = strrchr(rootpath_str, '/');

            if (last_slash) {
                wildcard = last_slash + 1;
                *last_slash = '\0';
                if (!wc_unescape(rootpath_unesc, rootpath_str)) {
                    scp_source_abort(scp, "scp: wildcards in path components "
                                     "before the file name not supported");
                    sfree(rootpath_str);
                    sfree(rootpath_unesc);
                    return;
                }

                pathname = ptrlen_from_asciz(rootpath_unesc);
            } else {
                pathname = PTRLEN_LITERAL(".");
                wildcard = rootpath_str;
            }
        }

        /*
         * Now we know what directory we're scanning, and what
         * wildcard (if any) we're using to match the filenames we get
         * back.
         */
        sftpsrv_stat(scp->sf, &scp->reply.srb, pathname, true);
        if (scp->reply.err) {
            scp_source_abort(
                scp, "%.*s: unable to access: %s",
                PTRLEN_PRINTF(pathname), scp->reply.errmsg);
            sfree(rootpath_str);
            sfree(rootpath_unesc);
            sfree(node);
            return;
        }

        scp_source_push_name(scp, pathname, scp->reply.attrs, wildcard);

        sfree(rootpath_str);
        sfree(rootpath_unesc);
        sfree(node);
        scp_requeue(scp);
        return;
    } else {
    }

    if (scp->head && scp->head->type == SCP_READFILE) {
        /*
         * Transfer file data if our backlog hasn't filled up.
         */
        int backlog;
        uint64_t limit = scp->file_size - scp->file_offset;
        if (limit > 4096)
            limit = 4096;
        if (limit > 0) {
            sftpsrv_read(scp->sf, &scp->reply.srb, scp->head->handle,
                         scp->file_offset, limit);
            if (scp->reply.err) {
                scp_source_abort(
                    scp, "%.*s: unable to read: %s",
                    PTRLEN_PRINTF(scp->head->pathname), scp->reply.errmsg);
                return;
            }

            backlog = sshfwd_write(
                scp->sc, scp->reply.data.ptr, scp->reply.data.len);
            scp->file_offset += scp->reply.data.len;

            if (backlog < SCP_MAX_BACKLOG)
                scp_requeue(scp);
            return;
        }

        /*
         * If we're done, send a terminating zero byte, close our file
         * handle, and pop the stack.
         */
        sshfwd_write(scp->sc, "\0", 1);
        sftpsrv_close(scp->sf, &scp->reply.srb, scp->head->handle);
        ScpSourceStackEntry *node = scp->head;
        scp->head = node->next;
        sfree(node);
        scp_requeue(scp);
        return;
    }

    /*
     * If our queue is actually empty, send outgoing EOF.
     */
    if (!scp->head) {
        sshfwd_send_exit_status(scp->sc, 0);
        sshfwd_write_eof(scp->sc);
        sshfwd_initiate_close(scp->sc, NULL);
        scp->finished = true;
        return;
    }

    /*
     * Otherwise, handle a command.
     */
    ScpSourceStackEntry *node = scp->head;
    scp->head = node->next;

    if (node->type == SCP_READDIR) {
        sftpsrv_readdir(scp->sf, &scp->reply.srb, node->handle, 1, true);
        if (scp->reply.err) {
            if (scp->reply.code != SSH_FX_EOF)
                scp_source_err(scp, "%.*s: unable to list directory: %s",
                               PTRLEN_PRINTF(node->pathname),
                               scp->reply.errmsg);
            sftpsrv_close(scp->sf, &scp->reply.srb, node->handle);

            if (!node->wildcard) {
                /*
                 * Send 'pop stack' or 'end of directory' command,
                 * unless this was the topmost READDIR in a
                 * wildcard-based retrieval (in which case we didn't
                 * send a D command to start, so an E now would have
                 * no stack entry to pop).
                 */
                scp_source_send_E(scp);
            }
        } else if (ptrlen_eq_string(scp->reply.name, ".") ||
                   ptrlen_eq_string(scp->reply.name, "..") ||
                   (node->wildcard &&
                    !wc_match_pl(node->wildcard, scp->reply.name))) {
            /* Skip special directory names . and .., and anything
             * that doesn't match our wildcard (if we have one). */
            scp->head = node;     /* put back the unfinished READDIR */
            node = NULL;          /* and prevent it being freed */
        } else {
            strbuf *subpath = strbuf_new();
            put_datapl(subpath, node->pathname);
            put_byte(subpath, '/');
            put_datapl(subpath, scp->reply.name);

            scp->head = node;     /* put back the unfinished READDIR */
            node = NULL;          /* and prevent it being freed */
            scp_source_push_name(scp, ptrlen_from_strbuf(subpath),
                                 scp->reply.attrs, NULL);

            strbuf_free(subpath);
        }
    } else if (node->attrs.permissions & PERMS_DIRECTORY) {
        assert(scp->recursive || node->wildcard);

        if (!node->wildcard)
            scp_source_send_CD(scp, 'D', node->attrs, 0, node->pathname);
        sftpsrv_opendir(scp->sf, &scp->reply.srb, node->pathname);
        if (scp->reply.err) {
            scp_source_err(
                scp, "%.*s: unable to access: %s",
                PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);

            if (!node->wildcard) {
                /* Send 'pop stack' or 'end of directory' command. */
                scp_source_send_E(scp);
            }
        } else {
            scp_source_push(
                scp, SCP_READDIR, node->pathname,
                scp->reply.handle, NULL, node->wildcard);
        }
    } else {
        sftpsrv_open(scp->sf, &scp->reply.srb,
                     node->pathname, SSH_FXF_READ, no_attrs);
        if (scp->reply.err) {
            scp_source_err(
                scp, "%.*s: unable to open: %s",
                PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);
            scp_requeue(scp);
            return;
        }
        sftpsrv_fstat(scp->sf, &scp->reply.srb, scp->reply.handle);
        if (scp->reply.err) {
            scp_source_err(
                scp, "%.*s: unable to stat: %s",
                PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);
            sftpsrv_close(scp->sf, &scp->reply.srb, scp->reply.handle);
            scp_requeue(scp);
            return;
        }
        scp->file_offset = 0;
        scp->file_size = scp->reply.attrs.size;
        scp_source_send_CD(scp, 'C', node->attrs,
                           scp->file_size, node->pathname);
        scp_source_push(
            scp, SCP_READFILE, node->pathname, scp->reply.handle, NULL, NULL);
    }
    sfree(node);
    scp_requeue(scp);
}

static size_t scp_source_send(ScpServer *s, const void *vdata, size_t length)
{
    ScpSource *scp = container_of(s, ScpSource, scpserver);
    const char *data = (const char *)vdata;
    size_t i;

    if (scp->finished)
        return 0;

    for (i = 0; i < length; i++) {
        if (scp->expect_newline) {
            if (data[i] == '\012') {
                /* End of an error message following a 1 byte */
                scp->expect_newline = false;
                scp->acks++;
            }
        } else {
            switch (data[i]) {
              case 0:                  /* ordinary ack */
                scp->acks++;
                break;
              case 1:                  /* non-fatal error; consume it */
                scp->expect_newline = true;
                break;
              case 2:
                scp_source_abort(
                    scp, "terminating on fatal error from client");
                return 0;
              default:
                scp_source_abort(
                    scp, "unrecognised response code from client");
                return 0;
            }
        }
    }

    scp_source_process_stack(scp);

    return 0;
}

static void scp_source_throttle(ScpServer *s, bool throttled)
{
    ScpSource *scp = container_of(s, ScpSource, scpserver);

    if (scp->finished)
        return;

    scp->throttled = throttled;
    if (!throttled)
        scp_source_process_stack(scp);
}

static void scp_source_eof(ScpServer *s)
{
    ScpSource *scp = container_of(s, ScpSource, scpserver);

    if (scp->finished)
        return;

    scp->eof = true;
    scp_source_process_stack(scp);
}

/* ----------------------------------------------------------------------
 * Sink end of the SCP protocol.
 */

typedef struct ScpSink ScpSink;
typedef struct ScpSinkStackEntry ScpSinkStackEntry;

struct ScpSink {
    SftpServer *sf;

    SshChannel *sc;
    ScpSinkStackEntry *head;

    uint64_t file_offset, file_size;
    unsigned long atime, mtime;
    bool got_file_times;

    bufchain data;
    bool input_eof;
    strbuf *command;
    char command_chr;

    strbuf *filename_sb;
    ptrlen filename;
    struct fxp_attrs attrs;

    char *errmsg;

    int crState;

    ScpReplyReceiver reply;

    ScpServer scpserver;
};

struct ScpSinkStackEntry {
    ScpSinkStackEntry *next;
    ptrlen destpath;

    /*
     * If isdir is true, then destpath identifies a directory that the
     * files we receive should be created inside. If it's false, then
     * it identifies the exact pathname the next file we receive
     * should be created _as_ - regardless of the filename in the 'C'
     * command.
     */
    bool isdir;
};

static void scp_sink_push(ScpSink *scp, ptrlen pathname, bool isdir)
{
    ScpSinkStackEntry *node = snew_plus(ScpSinkStackEntry, pathname.len);
    char *p = snew_plus_get_aux(node);

    node->destpath.ptr = p;
    node->destpath.len = pathname.len;
    memcpy(p, pathname.ptr, pathname.len);
    node->isdir = isdir;

    node->next = scp->head;
    scp->head = node;
}

static void scp_sink_pop(ScpSink *scp)
{
    ScpSinkStackEntry *node = scp->head;
    scp->head = node->next;
    sfree(node);
}

static void scp_sink_free(ScpServer *s);
static size_t scp_sink_send(ScpServer *s, const void *data, size_t length);
static void scp_sink_eof(ScpServer *s);
static void scp_sink_throttle(ScpServer *s, bool throttled) {}

static const ScpServerVtable ScpSink_ScpServer_vt = {
    .free = scp_sink_free,
    .send = scp_sink_send,
    .throttle = scp_sink_throttle,
    .eof = scp_sink_eof,
};

static void scp_sink_coroutine(ScpSink *scp);
static void scp_sink_start_callback(void *vscp)
{
    scp_sink_coroutine((ScpSink *)vscp);
}

static ScpSink *scp_sink_new(
    SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen pathname,
    bool pathname_is_definitely_dir)
{
    ScpSink *scp = snew(ScpSink);
    memset(scp, 0, sizeof(*scp));

    scp->scpserver.vt = &ScpSink_ScpServer_vt;
    scp_reply_setup(&scp->reply);
    scp->sc = sc;
    scp->sf = sftpsrv_new(sftpserver_vt);
    bufchain_init(&scp->data);
    scp->command = strbuf_new();
    scp->filename_sb = strbuf_new();

    if (!pathname_is_definitely_dir) {
        /*
         * If our root pathname is not already expected to be a
         * directory because of the -d option in the command line,
         * test it ourself to see whether it is or not.
         */
        sftpsrv_stat(scp->sf, &scp->reply.srb, pathname, true);
        if (!scp->reply.err &&
            (scp->reply.attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS) &&
            (scp->reply.attrs.permissions & PERMS_DIRECTORY))
            pathname_is_definitely_dir = true;
    }
    scp_sink_push(scp, pathname, pathname_is_definitely_dir);

    queue_toplevel_callback(scp_sink_start_callback, scp);

    return scp;
}

static void scp_sink_free(ScpServer *s)
{
    ScpSink *scp = container_of(s, ScpSink, scpserver);

    scp_reply_cleanup(&scp->reply);
    bufchain_clear(&scp->data);
    strbuf_free(scp->command);
    strbuf_free(scp->filename_sb);
    while (scp->head)
        scp_sink_pop(scp);
    sfree(scp->errmsg);

    delete_callbacks_for_context(scp);

    sfree(scp);
}

static void scp_sink_coroutine(ScpSink *scp)
{
    crBegin(scp->crState);

    while (1) {
        /*
         * Send an ack, and read a command.
         */
        sshfwd_write(scp->sc, "\0", 1);
        strbuf_clear(scp->command);
        while (1) {
            crMaybeWaitUntilV(scp->input_eof || bufchain_size(&scp->data) > 0);
            if (scp->input_eof)
                goto done;

            ptrlen data = bufchain_prefix(&scp->data);
            const char *cdata = data.ptr;
            const char *newline = memchr(cdata, '\012', data.len);
            if (newline)
                data.len = (int)(newline+1 - cdata);
            put_data(scp->command, cdata, data.len);
            bufchain_consume(&scp->data, data.len);

            if (newline)
                break;
        }

        /*
         * Parse the command.
         */
        strbuf_chomp(scp->command, '\n');
        scp->command_chr = scp->command->len > 0 ? scp->command->s[0] : '\0';
        if (scp->command_chr == 'T') {
            unsigned long dummy1, dummy2;
            if (sscanf(scp->command->s, "T%lu %lu %lu %lu",
                       &scp->mtime, &dummy1, &scp->atime, &dummy2) != 4)
                goto parse_error;
            scp->got_file_times = true;
        } else if (scp->command_chr == 'C' || scp->command_chr == 'D') {
            /*
             * Common handling of the start of this case, because the
             * messages are parsed similarly. We diverge later.
             */
            const char *q, *p = scp->command->s + 1; /* skip the 'C' */

            scp->attrs.flags = SSH_FILEXFER_ATTR_PERMISSIONS;
            scp->attrs.permissions = 0;
            while (*p >= '0' && *p <= '7') {
                scp->attrs.permissions =
                    scp->attrs.permissions * 8 + (*p - '0');
                p++;
            }
            if (*p != ' ')
                goto parse_error;
            p++;

            q = p;
            while (*p >= '0' && *p <= '9')
                p++;
            if (*p != ' ')
                goto parse_error;
            p++;
            scp->file_size = strtoull(q, NULL, 10);

            ptrlen leafname = make_ptrlen(
                p, scp->command->len - (p - scp->command->s));
            strbuf_clear(scp->filename_sb);
            put_datapl(scp->filename_sb, scp->head->destpath);
            if (scp->head->isdir) {
                if (scp->filename_sb->len > 0 &&
                    scp->filename_sb->s[scp->filename_sb->len-1]
                    != '/')
                    put_byte(scp->filename_sb, '/');
                put_datapl(scp->filename_sb, leafname);
            }
            scp->filename = ptrlen_from_strbuf(scp->filename_sb);

            if (scp->got_file_times) {
                scp->attrs.mtime = scp->mtime;
                scp->attrs.atime = scp->atime;
                scp->attrs.flags |= SSH_FILEXFER_ATTR_ACMODTIME;
            }
            scp->got_file_times = false;

            if (scp->command_chr == 'D') {
                sftpsrv_mkdir(scp->sf, &scp->reply.srb,
                              scp->filename, scp->attrs);

                if (scp->reply.err) {
                    scp->errmsg = dupprintf(
                        "'%.*s': unable to create directory: %s",
                        PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
                    goto done;
                }

                scp_sink_push(scp, scp->filename, true);
            } else {
                sftpsrv_open(scp->sf, &scp->reply.srb, scp->filename,
                             SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC,
                             scp->attrs);
                if (scp->reply.err) {
                    scp->errmsg = dupprintf(
                        "'%.*s': unable to open file: %s",
                        PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
                    goto done;
                }

                /*
                 * Now send an ack, and read the file data.
                 */
                sshfwd_write(scp->sc, "\0", 1);
                scp->file_offset = 0;
                while (scp->file_offset < scp->file_size) {
                    ptrlen data;
                    uint64_t this_len, remaining;

                    crMaybeWaitUntilV(
                        scp->input_eof || bufchain_size(&scp->data) > 0);
                    if (scp->input_eof) {
                        sftpsrv_close(scp->sf, &scp->reply.srb,
                                      scp->reply.handle);
                        goto done;
                    }

                    data = bufchain_prefix(&scp->data);
                    this_len = data.len;
                    remaining = scp->file_size - scp->file_offset;
                    if (this_len > remaining)
                        this_len = remaining;
                    sftpsrv_write(scp->sf, &scp->reply.srb,
                                  scp->reply.handle, scp->file_offset,
                                  make_ptrlen(data.ptr, this_len));
                    if (scp->reply.err) {
                        scp->errmsg = dupprintf(
                            "'%.*s': unable to write to file: %s",
                            PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
                        goto done;
                    }
                    bufchain_consume(&scp->data, this_len);
                    scp->file_offset += this_len;
                }

                /*
                 * Wait for the trailing NUL byte.
                 */
                crMaybeWaitUntilV(
                    scp->input_eof || bufchain_size(&scp->data) > 0);
                if (scp->input_eof) {
                    sftpsrv_close(scp->sf, &scp->reply.srb,
                                  scp->reply.handle);
                    goto done;
                }
                bufchain_consume(&scp->data, 1);
            }
        } else if (scp->command_chr == 'E') {
            if (!scp->head) {
                scp->errmsg = dupstr("received E command without matching D");
                goto done;
            }
            scp_sink_pop(scp);
            scp->got_file_times = false;
        } else {
            ptrlen cmd_pl;

            /*
             * Also come here if any of the above cases run into
             * parsing difficulties.
             */
          parse_error:
            cmd_pl = ptrlen_from_strbuf(scp->command);
            scp->errmsg = dupprintf("unrecognised scp command '%.*s'",
                                    PTRLEN_PRINTF(cmd_pl));
            goto done;
        }
    }

  done:
    if (scp->errmsg) {
        sshfwd_write_ext(scp->sc, true, scp->errmsg, strlen(scp->errmsg));
        sshfwd_write_ext(scp->sc, true, "\012", 1);
        sshfwd_send_exit_status(scp->sc, 1);
    } else {
        sshfwd_send_exit_status(scp->sc, 0);
    }
    sshfwd_write_eof(scp->sc);
    sshfwd_initiate_close(scp->sc, scp->errmsg);
    while (1) crReturnV;

    crFinishV;
}

static size_t scp_sink_send(ScpServer *s, const void *data, size_t length)
{
    ScpSink *scp = container_of(s, ScpSink, scpserver);

    if (!scp->input_eof) {
        bufchain_add(&scp->data, data, length);
        scp_sink_coroutine(scp);
    }
    return 0;
}

static void scp_sink_eof(ScpServer *s)
{
    ScpSink *scp = container_of(s, ScpSink, scpserver);

    scp->input_eof = true;
    scp_sink_coroutine(scp);
}

/* ----------------------------------------------------------------------
 * Top-level error handler, instantiated in the case where the user
 * sent a command starting with "scp " that we couldn't make sense of.
 */

typedef struct ScpError ScpError;

struct ScpError {
    SshChannel *sc;
    char *message;
    ScpServer scpserver;
};

static void scp_error_free(ScpServer *s);

static size_t scp_error_send(ScpServer *s, const void *data, size_t length)
{ return 0; }
static void scp_error_eof(ScpServer *s) {}
static void scp_error_throttle(ScpServer *s, bool throttled) {}

static const ScpServerVtable ScpError_ScpServer_vt = {
    .free = scp_error_free,
    .send = scp_error_send,
    .throttle = scp_error_throttle,
    .eof = scp_error_eof,
};

static void scp_error_send_message_cb(void *vscp)
{
    ScpError *scp = (ScpError *)vscp;
    sshfwd_write_ext(scp->sc, true, scp->message, strlen(scp->message));
    sshfwd_write_ext(scp->sc, true, "\n", 1);
    sshfwd_send_exit_status(scp->sc, 1);
    sshfwd_write_eof(scp->sc);
    sshfwd_initiate_close(scp->sc, scp->message);
}

static PRINTF_LIKE(2, 3) ScpError *scp_error_new(
    SshChannel *sc, const char *fmt, ...)
{
    va_list ap;
    ScpError *scp = snew(ScpError);

    memset(scp, 0, sizeof(*scp));

    scp->scpserver.vt = &ScpError_ScpServer_vt;
    scp->sc = sc;

    va_start(ap, fmt);
    scp->message = dupvprintf(fmt, ap);
    va_end(ap);

    queue_toplevel_callback(scp_error_send_message_cb, scp);

    return scp;
}

static void scp_error_free(ScpServer *s)
{
    ScpError *scp = container_of(s, ScpError, scpserver);

    sfree(scp->message);

    delete_callbacks_for_context(scp);

    sfree(scp);
}

/* ----------------------------------------------------------------------
 * Top-level entry point, which parses a command sent from the SSH
 * client, and if it recognises it as an scp command, instantiates an
 * appropriate ScpServer implementation and returns it.
 */

ScpServer *scp_recognise_exec(
    SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen command)
{
    bool recursive = false, preserve = false;
    bool targetshouldbedirectory = false;
    ptrlen command_orig = command;

    if (!ptrlen_startswith(command, PTRLEN_LITERAL("scp "), &command))
        return NULL;

    while (1) {
        if (ptrlen_startswith(command, PTRLEN_LITERAL("-v "), &command)) {
            /* Enable verbose mode in the server, which we ignore */
            continue;
        }
        if (ptrlen_startswith(command, PTRLEN_LITERAL("-r "), &command)) {
            recursive = true;
            continue;
        }
        if (ptrlen_startswith(command, PTRLEN_LITERAL("-p "), &command)) {
            preserve = true;
            continue;
        }
        if (ptrlen_startswith(command, PTRLEN_LITERAL("-d "), &command)) {
            targetshouldbedirectory = true;
            continue;
        }
        break;
    }

    if (ptrlen_startswith(command, PTRLEN_LITERAL("-t "), &command)) {
        ScpSink *scp = scp_sink_new(sc, sftpserver_vt, command,
                                    targetshouldbedirectory);
        return &scp->scpserver;
    } else if (ptrlen_startswith(command, PTRLEN_LITERAL("-f "), &command)) {
        ScpSource *scp = scp_source_new(sc, sftpserver_vt, command);
        scp->recursive = recursive;
        scp->send_file_times = preserve;
        return &scp->scpserver;
    } else {
        ScpError *scp = scp_error_new(
            sc, "Unable to parse scp command: '%.*s'",
            PTRLEN_PRINTF(command_orig));
        return &scp->scpserver;
    }
}
