/* Copyright (C) Daniel Stenberg
 * Copyright (C) Sara Golemon <sarag@libssh2.org>
 * 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 name of the copyright holder nor the names
 *   of any other 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 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.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include "libssh2_priv.h"

#include "channel.h"
#include "session.h"

#include <stdlib.h>  /* strtoll(), _strtoi64(), strtol() */

#if defined(HAVE_STRTOLL)
#define scpsize_strtol strtoll
#elif defined(HAVE_STRTOI64)
#define scpsize_strtol _strtoi64
#else
#define scpsize_strtol strtol
#endif

/* Max. length of a quoted string after libssh2_shell_quotearg() processing */
#define _libssh2_shell_quotedsize(s)     (3 * strlen(s) + 2)

/*
  This function quotes a string in a way suitable to be used with a
  shell, e.g. the file name
  one two
  becomes
  'one two'

  The resulting output string is crafted in a way that makes it usable
  with the two most common shell types: Bourne Shell derived shells
  (sh, ksh, ksh93, bash, zsh) and C-Shell derivates (csh, tcsh).

  The following special cases are handled:
  o  If the string contains an apostrophy itself, the apostrophy
  character is written in quotation marks, e.g. "'".
  The shell cannot handle the syntax 'doesn\'t', so we close the
  current argument word, add the apostrophe in quotation marks "",
  and open a new argument word instead (_ indicate the input
  string characters):
   _____   _   _
  'doesn' "'" 't'

  Sequences of apostrophes are combined in one pair of quotation marks:
  a'''b
  becomes
   _  ___  _
  'a'"'''"'b'

  o  If the string contains an exclamation mark (!), the C-Shell
  interprets it as an event number. Using \! (not within quotation
  marks or single quotation marks) is a mechanism understood by
  both Bourne Shell and C-Shell.

  If a quotation was already started, the argument word is closed
  first:
  a!b

  become
   _  _ _
  'a'\!'b'

  The result buffer must be large enough for the expanded result. A
  bad case regarding expansion is alternating characters and
  apostrophes:

  a'b'c'd'                   (length 8) gets converted to
  'a'"'"'b'"'"'c'"'"'d'"'"   (length 24)

  This is the worst case.

  Maximum length of the result:
  1 + 6 * (length(input) + 1) / 2) + 1

  => 3 * length(input) + 2

  Explanation:
  o  leading apostrophe
  o  one character / apostrophe pair (two characters) can get
  represented as 6 characters: a' -> a'"'"'
  o  String terminator (+1)

  A result buffer three times the size of the input buffer + 2
  characters should be safe.

  References:
  o  csh-compatible quotation (special handling for '!' etc.), see
  https://www.grymoire.com/Unix/Csh.html#toc-uh-10

  Return value:
  Length of the resulting string (not counting the terminating '\0'),
  or 0 in case of errors, e.g. result buffer too small

  Note: this function could possible be used elsewhere within libssh2, but
  until then it is kept static and in this source file.
*/

static size_t
shell_quotearg(const char *path, unsigned char *buf,
               size_t bufsize)
{
    const char *src;
    unsigned char *dst, *endp;

    /*
     * Processing States:
     *  UQSTRING:       unquoted string: ... -- used for quoting exclamation
     *                  marks. This is the initial state
     *  SQSTRING:       single-quoted-string: '... -- any character may follow
     *  QSTRING:        quoted string: "... -- only apostrophes may follow
     */
    enum { UQSTRING, SQSTRING, QSTRING } state = UQSTRING;

    endp = &buf[bufsize];
    src = path;
    dst = buf;
    while(*src && dst < endp - 1) {

        switch(*src) {
            /*
             * Special handling for apostrophe.
             * An apostrophe is always written in quotation marks, e.g.
             * ' -> "'".
             */

        case '\'':
            switch(state) {
            case UQSTRING:      /* Unquoted string */
                if(dst + 1 >= endp)
                    return 0;
                *dst++ = '"';
                break;
            case QSTRING:       /* Continue quoted string */
                break;
            case SQSTRING:      /* Close single quoted string */
                if(dst + 2 >= endp)
                    return 0;
                *dst++ = '\'';
                *dst++ = '"';
                break;
            default:
                break;
            }
            state = QSTRING;
            break;

            /*
             * Special handling for exclamation marks. CSH interprets
             * exclamation marks even when quoted with apostrophes. We convert
             * it to the plain string \!, because both Bourne Shell and CSH
             * interpret that as a verbatim exclamation mark.
             */

        case '!':
            switch(state) {
            case UQSTRING:
                if(dst + 1 >= endp)
                    return 0;
                *dst++ = '\\';
                break;
            case QSTRING:
                if(dst + 2 >= endp)
                    return 0;
                *dst++ = '"';           /* Closing quotation mark */
                *dst++ = '\\';
                break;
            case SQSTRING:              /* Close single quoted string */
                if(dst + 2 >= endp)
                    return 0;
                *dst++ = '\'';
                *dst++ = '\\';
                break;
            default:
                break;
            }
            state = UQSTRING;
            break;

            /*
             * Ordinary character: prefer single-quoted string
             */

        default:
            switch(state) {
            case UQSTRING:
                if(dst + 1 >= endp)
                    return 0;
                *dst++ = '\'';
                break;
            case QSTRING:
                if(dst + 2 >= endp)
                    return 0;
                *dst++ = '"';           /* Closing quotation mark */
                *dst++ = '\'';
                break;
            case SQSTRING:      /* Continue single quoted string */
                break;
            default:
                break;
            }
            state = SQSTRING;   /* Start single-quoted string */
            break;
        }

        if(dst + 1 >= endp)
            return 0;
        *dst++ = *src++;
    }

    switch(state) {
    case UQSTRING:
        break;
    case QSTRING:           /* Close quoted string */
        if(dst + 1 >= endp)
            return 0;
        *dst++ = '"';
        break;
    case SQSTRING:          /* Close single quoted string */
        if(dst + 1 >= endp)
            return 0;
        *dst++ = '\'';
        break;
    default:
        break;
    }

    if(dst + 1 >= endp)
        return 0;
    *dst = '\0';

    /* The result cannot be larger than 3 * strlen(path) + 2 */
    /* assert((dst - buf) <= (3 * (src - path) + 2)); */

    return dst - buf;
}

/*
 * scp_recv
 *
 * Open a channel and request a remote file via SCP
 *
 */
static LIBSSH2_CHANNEL *
scp_recv(LIBSSH2_SESSION * session, const char *path, libssh2_struct_stat * sb)
{
    size_t cmd_len;
    int rc;
    int tmp_err_code;
    const char *tmp_err_msg;

    if(session->scpRecv_state == libssh2_NB_state_idle) {
        session->scpRecv_mode = 0;
        session->scpRecv_size = 0;
        session->scpRecv_mtime = 0;
        session->scpRecv_atime = 0;

        session->scpRecv_command_len =
            _libssh2_shell_quotedsize(path) + sizeof("scp -f ") + (sb ? 1 : 0);

        session->scpRecv_command =
            LIBSSH2_ALLOC(session, session->scpRecv_command_len);

        if(!session->scpRecv_command) {
            _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                           "Unable to allocate a command buffer for "
                           "SCP session");
            return NULL;
        }

        snprintf((char *)session->scpRecv_command,
                 session->scpRecv_command_len,
                 "scp -%sf ", sb ? "p" : "");

        cmd_len = strlen((char *)session->scpRecv_command);

        if(!session->flag.quote_paths) {
            size_t path_len;

            path_len = strlen(path);

            /* no NUL-termination needed, so memcpy will do */
            memcpy(&session->scpRecv_command[cmd_len], path, path_len);
            cmd_len += path_len;
        }
        else {
            cmd_len += shell_quotearg(path,
                                      &session->scpRecv_command[cmd_len],
                                      session->scpRecv_command_len - cmd_len);
        }

        /* the command to exec should _not_ be NUL-terminated */
        session->scpRecv_command_len = cmd_len;

        _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                       "Opening channel for SCP receive"));

        session->scpRecv_state = libssh2_NB_state_created;
    }

    if(session->scpRecv_state == libssh2_NB_state_created) {
        /* Allocate a channel */
        session->scpRecv_channel =
            _libssh2_channel_open(session, "session",
                                  sizeof("session") - 1,
                                  LIBSSH2_CHANNEL_WINDOW_DEFAULT,
                                  LIBSSH2_CHANNEL_PACKET_DEFAULT, NULL,
                                  0);
        if(!session->scpRecv_channel) {
            if(libssh2_session_last_errno(session) !=
                LIBSSH2_ERROR_EAGAIN) {
                LIBSSH2_FREE(session, session->scpRecv_command);
                session->scpRecv_command = NULL;
                session->scpRecv_state = libssh2_NB_state_idle;
            }
            else {
                _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                               "Would block starting up channel");
            }
            return NULL;
        }

        session->scpRecv_state = libssh2_NB_state_sent;
    }

    if(session->scpRecv_state == libssh2_NB_state_sent) {
        /* Request SCP for the desired file */
        rc = _libssh2_channel_process_startup(session->scpRecv_channel, "exec",
                                              sizeof("exec") - 1,
                                              (char *)session->scpRecv_command,
                                              session->scpRecv_command_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block requesting SCP startup");
            return NULL;
        }
        else if(rc) {
            LIBSSH2_FREE(session, session->scpRecv_command);
            session->scpRecv_command = NULL;
            goto scp_recv_error;
        }
        LIBSSH2_FREE(session, session->scpRecv_command);
        session->scpRecv_command = NULL;

        _libssh2_debug((session, LIBSSH2_TRACE_SCP, "Sending initial wakeup"));
        /* SCP ACK */
        session->scpRecv_response[0] = '\0';

        session->scpRecv_state = libssh2_NB_state_sent1;
    }

    if(session->scpRecv_state == libssh2_NB_state_sent1) {
        rc = (int)_libssh2_channel_write(session->scpRecv_channel, 0,
                                         session->scpRecv_response, 1);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block sending initial wakeup");
            return NULL;
        }
        else if(rc != 1) {
            goto scp_recv_error;
        }

        /* Parse SCP response */
        session->scpRecv_response_len = 0;

        session->scpRecv_state = libssh2_NB_state_sent2;
    }

    if((session->scpRecv_state == libssh2_NB_state_sent2)
        || (session->scpRecv_state == libssh2_NB_state_sent3)) {
        while(sb && (session->scpRecv_response_len <
                     LIBSSH2_SCP_RESPONSE_BUFLEN)) {
            unsigned char *s, *p;

            if(session->scpRecv_state == libssh2_NB_state_sent2) {
                rc = (int)_libssh2_channel_read(session->scpRecv_channel, 0,
                                                (char *) session->
                                                scpRecv_response +
                                                session->scpRecv_response_len,
                                                1);
                if(rc == LIBSSH2_ERROR_EAGAIN) {
                    _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                                   "Would block waiting for SCP response");
                    return NULL;
                }
                else if(rc < 0) {
                    /* error, give up */
                    _libssh2_error(session, rc, "Failed reading SCP response");
                    goto scp_recv_error;
                }
                else if(rc == 0)
                    goto scp_recv_empty_channel;

                session->scpRecv_response_len++;

                if(session->scpRecv_response[0] != 'T') {
                    size_t err_len;
                    char *err_msg;

                    /* there can be
                       01 for warnings
                       02 for errors

                       The following string MUST be newline terminated
                    */
                    err_len =
                        _libssh2_channel_packet_data_len(session->
                                                         scpRecv_channel, 0);
                    err_msg = LIBSSH2_ALLOC(session, err_len + 1);
                    if(!err_msg) {
                        _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                                       "Failed to get memory ");
                        goto scp_recv_error;
                    }

                    /* Read the remote error message */
                    (void)_libssh2_channel_read(session->scpRecv_channel, 0,
                                                err_msg, err_len);
                    /* If it failed for any reason, we ignore it anyway. */

                    /* zero terminate the error */
                    err_msg[err_len] = 0;

                    _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                                   "got %02x %s", session->scpRecv_response[0],
                                   err_msg));

                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Failed to recv file");

                    LIBSSH2_FREE(session, err_msg);
                    goto scp_recv_error;
                }

                if((session->scpRecv_response_len > 1) &&
                    ((session->
                      scpRecv_response[session->scpRecv_response_len - 1] <
                      '0')
                     || (session->
                         scpRecv_response[session->scpRecv_response_len - 1] >
                         '9'))
                    && (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        ' ')
                    && (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        '\r')
                    && (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        '\n')) {
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid data in SCP response");
                    goto scp_recv_error;
                }

                if((session->scpRecv_response_len < 9)
                    || (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        '\n')) {
                    if(session->scpRecv_response_len ==
                        LIBSSH2_SCP_RESPONSE_BUFLEN) {
                        /* You had your chance */
                        _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                       "Unterminated response from "
                                       "SCP server");
                        goto scp_recv_error;
                    }
                    /* Way too short to be an SCP response, or not done yet,
                       short circuit */
                    continue;
                }

                /* We're guaranteed not to go under response_len == 0 by the
                   logic above */
                while((session->
                        scpRecv_response[session->scpRecv_response_len - 1] ==
                        '\r')
                       || (session->
                           scpRecv_response[session->scpRecv_response_len -
                                            1] == '\n'))
                    session->scpRecv_response_len--;
                session->scpRecv_response[session->scpRecv_response_len] =
                    '\0';

                if(session->scpRecv_response_len < 8) {
                    /* EOL came too soon */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "too short");
                    goto scp_recv_error;
                }

                s = session->scpRecv_response + 1;

                p = (unsigned char *) strchr((char *) s, ' ');
                if(!p || ((p - s) <= 0)) {
                    /* No spaces or space in the wrong spot */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "malformed mtime");
                    goto scp_recv_error;
                }

                *(p++) = '\0';
                /* Make sure we don't get fooled by leftover values */
                session->scpRecv_mtime = strtol((char *) s, NULL, 10);

                s = (unsigned char *) strchr((char *) p, ' ');
                if(!s || ((s - p) <= 0)) {
                    /* No spaces or space in the wrong spot */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "malformed mtime.usec");
                    goto scp_recv_error;
                }

                /* Ignore mtime.usec */
                s++;
                p = (unsigned char *) strchr((char *) s, ' ');
                if(!p || ((p - s) <= 0)) {
                    /* No spaces or space in the wrong spot */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "too short or malformed");
                    goto scp_recv_error;
                }

                *p = '\0';
                /* Make sure we don't get fooled by leftover values */
                session->scpRecv_atime = strtol((char *) s, NULL, 10);

                /* SCP ACK */
                session->scpRecv_response[0] = '\0';

                session->scpRecv_state = libssh2_NB_state_sent3;
            }

            if(session->scpRecv_state == libssh2_NB_state_sent3) {
                rc = (int)_libssh2_channel_write(session->scpRecv_channel, 0,
                                                 session->scpRecv_response, 1);
                if(rc == LIBSSH2_ERROR_EAGAIN) {
                    _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                                   "Would block waiting to send SCP ACK");
                    return NULL;
                }
                else if(rc != 1) {
                    goto scp_recv_error;
                }

                _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                               "mtime = %ld, atime = %ld",
                               session->scpRecv_mtime,
                               session->scpRecv_atime));

                /* We *should* check that atime.usec is valid, but why let
                   that stop use? */
                break;
            }
        }

        session->scpRecv_state = libssh2_NB_state_sent4;
    }

    if(session->scpRecv_state == libssh2_NB_state_sent4) {
        session->scpRecv_response_len = 0;

        session->scpRecv_state = libssh2_NB_state_sent5;
    }

    if((session->scpRecv_state == libssh2_NB_state_sent5)
        || (session->scpRecv_state == libssh2_NB_state_sent6)) {
        while(session->scpRecv_response_len < LIBSSH2_SCP_RESPONSE_BUFLEN) {
            char *s, *p, *e = NULL;

            if(session->scpRecv_state == libssh2_NB_state_sent5) {
                rc = (int)_libssh2_channel_read(session->scpRecv_channel, 0,
                                                (char *) session->
                                                scpRecv_response +
                                                session->scpRecv_response_len,
                                                1);
                if(rc == LIBSSH2_ERROR_EAGAIN) {
                    _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                                   "Would block waiting for SCP response");
                    return NULL;
                }
                else if(rc < 0) {
                    /* error, bail out */
                    _libssh2_error(session, rc, "Failed reading SCP response");
                    goto scp_recv_error;
                }
                else if(rc == 0)
                    goto scp_recv_empty_channel;

                session->scpRecv_response_len++;

                if(session->scpRecv_response[0] != 'C') {
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server");
                    goto scp_recv_error;
                }

                if((session->scpRecv_response_len > 1) &&
                    (session->
                     scpRecv_response[session->scpRecv_response_len - 1] !=
                     '\r')
                    && (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        '\n')
                    &&
                    (session->
                     scpRecv_response[session->scpRecv_response_len - 1]
                     < 32)) {
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid data in SCP response");
                    goto scp_recv_error;
                }

                if((session->scpRecv_response_len < 7)
                    || (session->
                        scpRecv_response[session->scpRecv_response_len - 1] !=
                        '\n')) {
                    if(session->scpRecv_response_len ==
                        LIBSSH2_SCP_RESPONSE_BUFLEN) {
                        /* You had your chance */
                        _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                       "Unterminated response "
                                       "from SCP server");
                        goto scp_recv_error;
                    }
                    /* Way too short to be an SCP response, or not done yet,
                       short circuit */
                    continue;
                }

                /* We're guaranteed not to go under response_len == 0 by the
                   logic above */
                while((session->
                        scpRecv_response[session->scpRecv_response_len - 1] ==
                        '\r')
                       || (session->
                           scpRecv_response[session->scpRecv_response_len -
                                            1] == '\n')) {
                    session->scpRecv_response_len--;
                }
                session->scpRecv_response[session->scpRecv_response_len] =
                    '\0';

                if(session->scpRecv_response_len < 6) {
                    /* EOL came too soon */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "too short");
                    goto scp_recv_error;
                }

                s = (char *) session->scpRecv_response + 1;

                p = strchr(s, ' ');
                if(!p || ((p - s) <= 0)) {
                    /* No spaces or space in the wrong spot */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "malformed mode");
                    goto scp_recv_error;
                }

                *(p++) = '\0';
                /* Make sure we don't get fooled by leftover values */
                session->scpRecv_mode = strtol(s, &e, 8);
                if(e && *e) {
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "invalid mode");
                    goto scp_recv_error;
                }

                s = strchr(p, ' ');
                if(!s || ((s - p) <= 0)) {
                    /* No spaces or space in the wrong spot */
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "too short or malformed");
                    goto scp_recv_error;
                }

                *s = '\0';
                /* Make sure we don't get fooled by leftover values */
                session->scpRecv_size = scpsize_strtol(p, &e, 10);
                if(e && *e) {
                    _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                                   "Invalid response from SCP server, "
                                   "invalid size");
                    goto scp_recv_error;
                }

                /* SCP ACK */
                session->scpRecv_response[0] = '\0';

                session->scpRecv_state = libssh2_NB_state_sent6;
            }

            if(session->scpRecv_state == libssh2_NB_state_sent6) {
                rc = (int)_libssh2_channel_write(session->scpRecv_channel, 0,
                                                 session->scpRecv_response, 1);
                if(rc == LIBSSH2_ERROR_EAGAIN) {
                    _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                                   "Would block sending SCP ACK");
                    return NULL;
                }
                else if(rc != 1) {
                    goto scp_recv_error;
                }
                _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                               "mode = 0%lo size = %ld", session->scpRecv_mode,
                               (long)session->scpRecv_size));

                /* We *should* check that basename is valid, but why let that
                   stop us? */
                break;
            }
        }

        session->scpRecv_state = libssh2_NB_state_sent7;
    }

    if(sb) {
        memset(sb, 0, sizeof(libssh2_struct_stat));

        sb->st_mtime = session->scpRecv_mtime;
        sb->st_atime = session->scpRecv_atime;
        sb->st_size = (libssh2_struct_stat_size)session->scpRecv_size;
        sb->st_mode = (unsigned short)session->scpRecv_mode;
    }

    session->scpRecv_state = libssh2_NB_state_idle;
    return session->scpRecv_channel;

scp_recv_empty_channel:
    /* the code only jumps here as a result of a zero read from channel_read()
       so we check EOF status to avoid getting stuck in a loop */
    if(libssh2_channel_eof(session->scpRecv_channel))
        _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                       "Unexpected channel close");
    else
        return session->scpRecv_channel;
    /* fall-through */
scp_recv_error:
    tmp_err_code = session->err_code;
    tmp_err_msg = session->err_msg;
    while(libssh2_channel_free(session->scpRecv_channel) ==
          LIBSSH2_ERROR_EAGAIN);
    session->err_code = tmp_err_code;
    session->err_msg = tmp_err_msg;
    session->scpRecv_channel = NULL;
    session->scpRecv_state = libssh2_NB_state_idle;
    return NULL;
}

#ifndef LIBSSH2_NO_DEPRECATED
/*
 * libssh2_scp_recv
 *
 * DEPRECATED
 *
 * Open a channel and request a remote file via SCP.  This receives files
 * larger than 2 GB, but is unable to report the proper size on platforms
 * where the st_size member of struct stat is limited to 2 GB (e.g. windows).
 *
 */
LIBSSH2_API LIBSSH2_CHANNEL *
libssh2_scp_recv(LIBSSH2_SESSION *session, const char *path, struct stat *sb)
{
    LIBSSH2_CHANNEL *ptr;

    /* scp_recv uses libssh2_struct_stat, so pass one if the caller gave us a
       struct to populate... */
    libssh2_struct_stat sb_intl;
    libssh2_struct_stat *sb_ptr;
    memset(&sb_intl, 0, sizeof(sb_intl));
    sb_ptr = sb ? &sb_intl : NULL;

    BLOCK_ADJUST_ERRNO(ptr, session, scp_recv(session, path, sb_ptr));

    /* ...and populate the caller's with as much info as fits. */
    if(sb) {
        memset(sb, 0, sizeof(struct stat));

        sb->st_mtime = sb_intl.st_mtime;
        sb->st_atime = sb_intl.st_atime;
        sb->st_size = (off_t)sb_intl.st_size;
        sb->st_mode = sb_intl.st_mode;
    }

    return ptr;
}
#endif

/*
 * libssh2_scp_recv2
 *
 * Open a channel and request a remote file via SCP.  This supports files > 2GB
 * on platforms that support it.
 *
 */
LIBSSH2_API LIBSSH2_CHANNEL *
libssh2_scp_recv2(LIBSSH2_SESSION *session, const char *path,
                  libssh2_struct_stat *sb)
{
    LIBSSH2_CHANNEL *ptr;
    BLOCK_ADJUST_ERRNO(ptr, session, scp_recv(session, path, sb));
    return ptr;
}

/*
 * scp_send
 *
 * Send a file using SCP
 *
 */
static LIBSSH2_CHANNEL *
scp_send(LIBSSH2_SESSION * session, const char *path, int mode,
         libssh2_int64_t size, time_t mtime, time_t atime)
{
    size_t cmd_len;
    int rc;
    int tmp_err_code;
    const char *tmp_err_msg;

    if(session->scpSend_state == libssh2_NB_state_idle) {
        session->scpSend_command_len =
            _libssh2_shell_quotedsize(path) + sizeof("scp -t ") +
            ((mtime || atime) ? 1 : 0);

        session->scpSend_command =
            LIBSSH2_ALLOC(session, session->scpSend_command_len);

        if(!session->scpSend_command) {
            _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                           "Unable to allocate a command buffer for "
                           "SCP session");
            return NULL;
        }

        snprintf((char *)session->scpSend_command,
                 session->scpSend_command_len,
                 "scp -%st ", (mtime || atime) ? "p" : "");

        cmd_len = strlen((char *)session->scpSend_command);

        if(!session->flag.quote_paths) {
            size_t path_len;

            path_len = strlen(path);

            /* no NUL-termination needed, so memcpy will do */
            memcpy(&session->scpSend_command[cmd_len], path, path_len);
            cmd_len += path_len;

        }
        else {
            cmd_len += shell_quotearg(path,
                                      &session->scpSend_command[cmd_len],
                                      session->scpSend_command_len - cmd_len);
        }

        /* the command to exec should _not_ be NUL-terminated */
        session->scpSend_command_len = cmd_len;

        _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                       "Opening channel for SCP send"));
        /* Allocate a channel */

        session->scpSend_state = libssh2_NB_state_created;
    }

    if(session->scpSend_state == libssh2_NB_state_created) {
        session->scpSend_channel =
            _libssh2_channel_open(session, "session", sizeof("session") - 1,
                                  LIBSSH2_CHANNEL_WINDOW_DEFAULT,
                                  LIBSSH2_CHANNEL_PACKET_DEFAULT, NULL, 0);
        if(!session->scpSend_channel) {
            if(libssh2_session_last_errno(session) != LIBSSH2_ERROR_EAGAIN) {
                /* previous call set libssh2_session_last_error(), pass it
                   through */
                LIBSSH2_FREE(session, session->scpSend_command);
                session->scpSend_command = NULL;
                session->scpSend_state = libssh2_NB_state_idle;
            }
            else {
                _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                               "Would block starting up channel");
            }
            return NULL;
        }

        session->scpSend_state = libssh2_NB_state_sent;
    }

    if(session->scpSend_state == libssh2_NB_state_sent) {
        /* Request SCP for the desired file */
        rc = _libssh2_channel_process_startup(session->scpSend_channel, "exec",
                                              sizeof("exec") - 1,
                                              (char *)session->scpSend_command,
                                              session->scpSend_command_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block requesting SCP startup");
            return NULL;
        }
        else if(rc) {
            /* previous call set libssh2_session_last_error(), pass it
               through */
            LIBSSH2_FREE(session, session->scpSend_command);
            session->scpSend_command = NULL;
            _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                           "Unknown error while getting error string");
            goto scp_send_error;
        }
        LIBSSH2_FREE(session, session->scpSend_command);
        session->scpSend_command = NULL;

        session->scpSend_state = libssh2_NB_state_sent1;
    }

    if(session->scpSend_state == libssh2_NB_state_sent1) {
        /* Wait for ACK */
        rc = (int)_libssh2_channel_read(session->scpSend_channel, 0,
                                        (char *) session->scpSend_response, 1);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block waiting for response from remote");
            return NULL;
        }
        else if(rc < 0) {
            _libssh2_error(session, rc, "SCP failure");
            goto scp_send_error;
        }
        else if(!rc)
            /* remain in the same state */
            goto scp_send_empty_channel;
        else if(session->scpSend_response[0]) {
            _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                           "Invalid ACK response from remote");
            goto scp_send_error;
        }
        if(mtime || atime) {
            /* Send mtime and atime to be used for file */
            session->scpSend_response_len =
                snprintf((char *) session->scpSend_response,
                         LIBSSH2_SCP_RESPONSE_BUFLEN, "T%ld 0 %ld 0\n",
                         (long)mtime, (long)atime);
            _libssh2_debug((session, LIBSSH2_TRACE_SCP, "Sent %s",
                           session->scpSend_response));
        }

        session->scpSend_state = libssh2_NB_state_sent2;
    }

    /* Send mtime and atime to be used for file */
    if(mtime || atime) {
        if(session->scpSend_state == libssh2_NB_state_sent2) {
            rc = (int)_libssh2_channel_write(session->scpSend_channel, 0,
                                             session->scpSend_response,
                                             session->scpSend_response_len);
            if(rc == LIBSSH2_ERROR_EAGAIN) {
                _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                               "Would block sending time data for SCP file");
                return NULL;
            }
            else if(rc != (int)session->scpSend_response_len) {
                _libssh2_error(session, LIBSSH2_ERROR_SOCKET_SEND,
                               "Unable to send time data for SCP file");
                goto scp_send_error;
            }

            session->scpSend_state = libssh2_NB_state_sent3;
        }

        if(session->scpSend_state == libssh2_NB_state_sent3) {
            /* Wait for ACK */
            rc = (int)_libssh2_channel_read(session->scpSend_channel, 0,
                                            (char *) session->scpSend_response,
                                            1);
            if(rc == LIBSSH2_ERROR_EAGAIN) {
                _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                               "Would block waiting for response");
                return NULL;
            }
            else if(rc < 0) {
                _libssh2_error(session, rc, "SCP failure");
                goto scp_send_error;
            }
            else if(!rc)
                /* remain in the same state */
                goto scp_send_empty_channel;
            else if(session->scpSend_response[0]) {
                _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                               "Invalid SCP ACK response");
                goto scp_send_error;
            }

            session->scpSend_state = libssh2_NB_state_sent4;
        }
    }
    else {
        if(session->scpSend_state == libssh2_NB_state_sent2) {
            session->scpSend_state = libssh2_NB_state_sent4;
        }
    }

    if(session->scpSend_state == libssh2_NB_state_sent4) {
        /* Send mode, size, and basename */
        const char *base = strrchr(path, '/');
        if(base)
            base++;
        else
            base = path;

        session->scpSend_response_len =
            snprintf((char *) session->scpSend_response,
                     LIBSSH2_SCP_RESPONSE_BUFLEN, "C0%o %"
                     LIBSSH2_INT64_T_FORMAT " %s\n", mode,
                     size, base);
        _libssh2_debug((session, LIBSSH2_TRACE_SCP, "Sent %s",
                       session->scpSend_response));

        session->scpSend_state = libssh2_NB_state_sent5;
    }

    if(session->scpSend_state == libssh2_NB_state_sent5) {
        rc = (int)_libssh2_channel_write(session->scpSend_channel, 0,
                                         session->scpSend_response,
                                         session->scpSend_response_len);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block send core file data for SCP file");
            return NULL;
        }
        else if(rc != (int)session->scpSend_response_len) {
            _libssh2_error(session, LIBSSH2_ERROR_SOCKET_SEND,
                           "Unable to send core file data for SCP file");
            goto scp_send_error;
        }

        session->scpSend_state = libssh2_NB_state_sent6;
    }

    if(session->scpSend_state == libssh2_NB_state_sent6) {
        /* Wait for ACK */
        rc = (int)_libssh2_channel_read(session->scpSend_channel, 0,
                                        (char *) session->scpSend_response,
                                        1);
        if(rc == LIBSSH2_ERROR_EAGAIN) {
            _libssh2_error(session, LIBSSH2_ERROR_EAGAIN,
                           "Would block waiting for response");
            return NULL;
        }
        else if(rc < 0) {
            _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                           "Invalid ACK response from remote");
            goto scp_send_error;
        }
        else if(rc == 0)
            goto scp_send_empty_channel;

        else if(session->scpSend_response[0]) {
            size_t err_len;
            char *err_msg;

            err_len =
                _libssh2_channel_packet_data_len(session->scpSend_channel, 0);
            err_msg = LIBSSH2_ALLOC(session, err_len + 1);
            if(!err_msg) {
                _libssh2_error(session, LIBSSH2_ERROR_ALLOC,
                               "failed to get memory");
                goto scp_send_error;
            }

            /* Read the remote error message */
            rc = (int)_libssh2_channel_read(session->scpSend_channel, 0,
                                            err_msg, err_len);
            if(rc > 0) {
                err_msg[err_len] = 0;
                _libssh2_debug((session, LIBSSH2_TRACE_SCP,
                               "got %02x %s", session->scpSend_response[0],
                               err_msg));
            }
            LIBSSH2_FREE(session, err_msg);
            _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                           "failed to send file");
            goto scp_send_error;
        }
    }

    session->scpSend_state = libssh2_NB_state_idle;
    return session->scpSend_channel;

scp_send_empty_channel:
    /* the code only jumps here as a result of a zero read from channel_read()
       so we check EOF status to avoid getting stuck in a loop */
    if(libssh2_channel_eof(session->scpSend_channel)) {
        _libssh2_error(session, LIBSSH2_ERROR_SCP_PROTOCOL,
                       "Unexpected channel close");
    }
    else
        return session->scpSend_channel;
    /* fall-through */
scp_send_error:
    tmp_err_code = session->err_code;
    tmp_err_msg = session->err_msg;
    while(libssh2_channel_free(session->scpSend_channel) ==
          LIBSSH2_ERROR_EAGAIN);
    session->err_code = tmp_err_code;
    session->err_msg = tmp_err_msg;
    session->scpSend_channel = NULL;
    session->scpSend_state = libssh2_NB_state_idle;
    return NULL;
}

/*
 * libssh2_scp_send_ex
 *
 * Send a file using SCP. Old API.
 */
LIBSSH2_API LIBSSH2_CHANNEL *
libssh2_scp_send_ex(LIBSSH2_SESSION *session, const char *path, int mode,
                    size_t size, long mtime, long atime)
{
    LIBSSH2_CHANNEL *ptr;
    BLOCK_ADJUST_ERRNO(ptr, session,
                       scp_send(session, path, mode, size,
                                (time_t)mtime, (time_t)atime));
    return ptr;
}

/*
 * libssh2_scp_send64
 *
 * Send a file using SCP
 */
LIBSSH2_API LIBSSH2_CHANNEL *
libssh2_scp_send64(LIBSSH2_SESSION *session, const char *path, int mode,
                   libssh2_int64_t size, time_t mtime, time_t atime)
{
    LIBSSH2_CHANNEL *ptr;
    BLOCK_ADJUST_ERRNO(ptr, session,
                       scp_send(session, path, mode, size, mtime, atime));
    return ptr;
}
