//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
//

using Google.Solutions.Common.Util;
using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Text;

#if DEBUG
using System.Threading;
#endif

#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments
#pragma warning disable CA1419 // Provide a parameterless constructor 
#pragma warning disable IDE0049 // Simplify Names

namespace Google.Solutions.Ssh.Native
{
    public enum LIBSSH2_HOSTKEY_HASH : Int32
    {
        MD5 = 1,
        SHA1 = 2,
        SHA256 = 3
    }

    internal enum LIBSSH2_METHOD : Int32
    {
        KEX = 0,
        HOSTKEY = 1,
        CRYPT_CS = 2, // Client -> Server
        CRYPT_SC = 3, // Server -> Client
        MAC_CS = 4,   // Client -> Server
        MAC_SC = 5,   // Server -> Client
        COMP_CS = 6,  // Compression Client -> Server
        COMP_SC = 7,  // Compression Server -> Client
        LANG_CS = 8,  // Client -> Server
        LANG_SC = 9,  // Server -> Client
    }

    [Flags]
    [SuppressMessage("Naming", "CA1714:Flags enums should have plural names")]
    internal enum LIBSSH2_TRACE : Int32
    {
        TRANS = (1 << 1),
        KEX = (1 << 2),
        AUTH = (1 << 3),
        CONN = (1 << 4),
        SCP = (1 << 5),
        SFTP = (1 << 6),
        ERROR = (1 << 7),
        internalKEY = (1 << 8),
        SOCKET = (1 << 9)
    }

    internal enum LIBSSH2_HOSTKEY_TYPE : Int32
    {
        UNKNOWN = 0,
        RSA = 1,
        DSS = 2,
        ECDSA_256 = 3,
        ECDSA_384 = 4,
        ECDSA_521 = 5,
        ED25519 = 6
    }

    internal enum SSH_DISCONNECT : Int32
    {
        HOST_NOT_ALLOWED_TO_CONNECT = 1,
        PROTOCOL_ERROR = 2,
        KEY_EXCHANGE_FAILED = 3,
        RESERVED = 4,
        MAC_ERROR = 5,
        COMPRESSION_ERROR = 6,
        SERVICE_NOT_AVAILABLE = 7,
        PROTOCOL_VERSION_NOT_SUPPORTED = 8,
        HOST_KEY_NOT_VERIFIABLE = 9,
        CONNECTION_LOST = 10,
        BY_APPLICATION = 11,
        TOO_MANY_CONNECTIONS = 12,
        AUTH_CANCELLED_BY_USER = 13,
        NO_MORE_AUTH_METHODS_AVAILABLE = 14,
        ILLEGAL_USER_NAME = 15
    }

    internal enum LIBSSH2_CHANNEL_EXTENDED_DATA : Int32
    {
        NORMAL = 0,
        IGNORE = 1,
        MERGE = 2,
    }

    public enum LIBSSH2_STREAM : Int32
    {
        NORMAL = 0,
        EXTENDED_DATA_STDERR = 1
    }

    /// <summary>
    /// FTP File Transfer Flags.
    /// </summary>
    [Flags]
    internal enum LIBSSH2_FXF_FLAGS : Int32
    {
        READ = 0x00000001,
        WRITE = 0x00000002,
        APPEND = 0x00000004,
        CREAT = 0x00000008,
        TRUNC = 0x00000010,
        EXCL = 0x00000020
    }

    internal enum LIBSSH2_OPENTYPE : Int32
    {
        OPENFILE = 0,
        OPENDIR = 1
    }

    /// <summary>
    /// SFTP attribute flag bits
    /// </summary>
    internal enum LIBSSH2_SFTP_ATTR : uint
    {
        SIZE = 0x00000001,
        UIDGID = 0x00000002,
        PERMISSIONS = 0x00000004,
        ACMODTIME = 0x00000008,
        EXTENDED = 0x80000000,
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct LIBSSH2_SFTP_ATTRIBUTES
    {
        /// <summary>
        /// If flags & ATTR_* bit is set, then the value in this struct is
        /// meaningful. Otherwise it should be ignored.
        /// </summary>
        internal LIBSSH2_SFTP_ATTR flags;
        internal ulong filesize;
        internal uint uid;
        internal uint gid;
        internal FilePermissions permissions;
        internal uint atime;
        internal uint mtime;
    };

    internal static class NativeMethods
    {
        private const string Libssh2 = "libssh2.dll";

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_init(Int32 flags);

        //---------------------------------------------------------------------
        // Session functions.
        //---------------------------------------------------------------------

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate IntPtr Alloc(
            IntPtr size,
            IntPtr context);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate IntPtr Realloc(
            IntPtr ptr,
            IntPtr size,
            IntPtr context);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate void Free(
            IntPtr ptr,
            IntPtr context);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Libssh2SessionHandle libssh2_session_init_ex(
            Alloc alloc,
            Free free,
            Realloc realloc,
            IntPtr context);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_free(
            Libssh2SessionHandle session,
            IntPtr ptr);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_session_free(
            IntPtr session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern Int32 libssh2_session_disconnect_ex(
            Libssh2SessionHandle session,
            SSH_DISCONNECT reason,
            [MarshalAs(UnmanagedType.LPStr)] string? description,
            [MarshalAs(UnmanagedType.LPStr)] string? lang);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_session_get_blocking(
            Libssh2SessionHandle session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern void libssh2_session_set_blocking(
            Libssh2SessionHandle session,
            Int32 blocking);

        //---------------------------------------------------------------------
        // Algorithm functions.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern IntPtr libssh2_session_methods(
            Libssh2SessionHandle session,
            LIBSSH2_METHOD methodType);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_session_supported_algs(
            Libssh2SessionHandle session,
            LIBSSH2_METHOD methodType,
            [Out] out IntPtr algorithmsPtrPtr);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern Int32 libssh2_session_method_pref(
            Libssh2SessionHandle session,
            LIBSSH2_METHOD methodType,
            [MarshalAs(UnmanagedType.LPStr)] string prefs);


        //---------------------------------------------------------------------
        // Banner functions.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern IntPtr libssh2_session_banner_get(
            Libssh2SessionHandle session);


        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern Int32 libssh2_session_banner_set(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string banner);

        //---------------------------------------------------------------------
        // Handshake functions.
        //---------------------------------------------------------------------

        //
        // NB. This function hangs when using libssh2 1.9.0 on Windows 10 1903.
        // https://github.com/libssh2/libssh2/issues/388
        //
        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_session_handshake(
            Libssh2SessionHandle session,
            IntPtr socket);

        //---------------------------------------------------------------------
        // Hostkey functions.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern IntPtr libssh2_session_hostkey(
            Libssh2SessionHandle session,
            out IntPtr length,
            out LIBSSH2_HOSTKEY_TYPE type);


        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern IntPtr libssh2_hostkey_hash(
            Libssh2SessionHandle session,
            LIBSSH2_HOSTKEY_HASH hashType);

        //---------------------------------------------------------------------
        // Timeout.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_session_get_timeout(
            Libssh2SessionHandle session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern void libssh2_session_set_timeout(
            Libssh2SessionHandle session,
            int timeout);

        //---------------------------------------------------------------------
        // User auth.
        //
        // NB. The documentation on libssh2_userauth_publickey is extremely sparse.
        // For a usage example, see:
        // https://github.com/stuntbadger/GuacamoleServer/blob/master/src/common-ssh/ssh.c
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Int32 libssh2_userauth_authenticated(
            Libssh2SessionHandle session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern IntPtr libssh2_userauth_list(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string username,
            int usernameLength);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate int SignCallback(
            IntPtr session,
            out IntPtr signature,
            out IntPtr signatureLength,
            IntPtr data,
            IntPtr dataLength,
            IntPtr context);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_userauth_publickey(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string username,
            [MarshalAs(UnmanagedType.LPArray)] byte[] publicKey,
            IntPtr pemPublicKeyLength,
            SignCallback callback,
            IntPtr context);

        [StructLayout(LayoutKind.Sequential)]
        internal struct LIBSSH2_USERAUTH_KBDINT_PROMPT
        {
            internal IntPtr TextPtr;
            internal int TextLength;
            internal byte Echo;
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct LIBSSH2_USERAUTH_KBDINT_RESPONSE
        {
            internal IntPtr TextPtr;
            internal int TextLength;
        }

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate void KeyboardInteractiveCallback(
            IntPtr namePtr,
            int nameLength,
            IntPtr instructionPtr,
            int instructionLength,
            int numPrompts,
            IntPtr prompts,
            IntPtr responses,
            IntPtr context);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_userauth_keyboard_interactive_ex(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string username,
            int usernameLength,
            KeyboardInteractiveCallback callback,
            IntPtr context);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate int PasswordChangeCallback(
            IntPtr session,
            IntPtr newPasswordPtr,
            IntPtr newPasswordLengthPtr,
            IntPtr context);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_userauth_password_ex(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string username,
            int usernameLength,
            [MarshalAs(UnmanagedType.LPStr)] string password,
            int passwordLength,
            PasswordChangeCallback passwordChangeCallback);

        //---------------------------------------------------------------------
        // Channel.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_close(
            Libssh2ChannelHandle channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_free(
            IntPtr channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern Libssh2ChannelHandle libssh2_channel_open_ex(
            Libssh2SessionHandle session,
            [MarshalAs(UnmanagedType.LPStr)] string channelType,
            uint channelTypeLength,
            uint windowSize,
            uint packetSize,
            [MarshalAs(UnmanagedType.LPStr)] string? message,
            uint messageLength);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_channel_setenv_ex(
            Libssh2ChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string variableName,
            uint variableNameLength,
            [MarshalAs(UnmanagedType.LPStr)] string variableValue,
            uint variableValueLength);


        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_channel_process_startup(
            Libssh2ChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string request,
            uint requestLength,
            [MarshalAs(UnmanagedType.LPStr)] string? message,
            uint messageLength);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_read_ex(
            Libssh2ChannelHandle channel,
            int streamId,
            byte[] buffer,
            IntPtr bufferSize);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_write_ex(
            Libssh2ChannelHandle channel,
            int streamId,
            byte[] buffer,
            IntPtr bufferSize);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_wait_closed(
            Libssh2ChannelHandle channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_wait_eof(
            Libssh2ChannelHandle channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_get_exit_status(
            Libssh2ChannelHandle channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_get_exit_signal(
            Libssh2ChannelHandle channel,
            out IntPtr exitsignal,
            out IntPtr exitsignalLength,
            out IntPtr errmsg,
            out IntPtr errmsgLength,
            out IntPtr langTag,
            out IntPtr langTagLength);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_handle_extended_data2(
            Libssh2ChannelHandle channel,
            LIBSSH2_CHANNEL_EXTENDED_DATA mode);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_eof(
            Libssh2ChannelHandle channel);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        internal static extern int libssh2_channel_request_pty_ex(
            Libssh2ChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string term,
            uint termLength,
            byte[]? modes,
            uint modesLength,
            int width,
            int height,
            int widthPx,
            int heightPx);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_channel_request_pty_size_ex(
            Libssh2ChannelHandle channel,
            int width,
            int height,
            int widthPx,
            int heightPx);

        //---------------------------------------------------------------------
        // SFTP.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Libssh2SftpChannelHandle libssh2_sftp_init(
            Libssh2SessionHandle session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_shutdown(
            IntPtr sftp);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_last_error(
            Libssh2SftpChannelHandle sftp);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_close_handle(
            IntPtr sftp);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern Libssh2SftpFileHandle libssh2_sftp_open_ex(
            Libssh2SftpChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string path,
            uint pathLength,
            LIBSSH2_FXF_FLAGS flags,
            FilePermissions mode,
            LIBSSH2_OPENTYPE openType);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_readdir_ex(
            Libssh2SftpFileHandle handle,
            IntPtr buffer,
            IntPtr bufferSize,
            IntPtr longEntry,
            IntPtr longEntrySize,
            out LIBSSH2_SFTP_ATTRIBUTES attrs);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_mkdir_ex(
            Libssh2SftpChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string path,
            uint pathLength,
            FilePermissions mode);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_rmdir_ex(
            Libssh2SftpChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string path,
            uint pathLength);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_unlink_ex(
            Libssh2SftpChannelHandle channel,
            [MarshalAs(UnmanagedType.LPStr)] string path,
            uint pathLength);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_read(
            Libssh2SftpFileHandle channel,
            byte[] buffer,
            IntPtr bufferSize);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_write(
            Libssh2SftpFileHandle channel,
            IntPtr buffer,
            IntPtr bufferSize);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_sftp_fstat_ex(
            Libssh2SftpFileHandle channel,
            out LIBSSH2_SFTP_ATTRIBUTES attrs,
            int setstat);

        //---------------------------------------------------------------------
        // Keepalive.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern void libssh2_keepalive_config(
            Libssh2SessionHandle session,
            int wantReply,
            uint interval);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_keepalive_send(
            Libssh2SessionHandle session,
            out int secondsToNext);

        //---------------------------------------------------------------------
        // Error functions.
        //---------------------------------------------------------------------

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_session_last_errno(
            Libssh2SessionHandle session);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int libssh2_session_last_error(
            Libssh2SessionHandle session,
            out IntPtr errorMessage,
            out int errorMessageLength,
            int allocateBuffer);

        //---------------------------------------------------------------------
        // Tracing functions.
        //---------------------------------------------------------------------

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate void TraceHandler(
            IntPtr sessionHandle,
            IntPtr context,
            IntPtr data,
            IntPtr length);

        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern void libssh2_trace(
            Libssh2SessionHandle session,
            LIBSSH2_TRACE bitmask);


        [DllImport(Libssh2, CallingConvention = CallingConvention.Cdecl)]
        internal static extern void libssh2_trace_sethandler(
            Libssh2SessionHandle session,
            IntPtr context,
            TraceHandler? callback);

        //---------------------------------------------------------------------
        // Winsock.
        //---------------------------------------------------------------------

        internal const uint WSA_WAIT_FAILED = 0xFFFFFFFF;
        internal const uint WSA_WAIT_EVENT_0 = 0;
        internal const uint WSA_WAIT_TIMEOUT = 0x102;
        internal const uint FD_READ = 1;
        internal const uint FD_WRITE = 2;
        internal const uint FD_OOB = 4;
        internal const uint FD_ACCEPT = 8;
        internal const uint FD_CONNECT = 16;
        internal const uint FD_CLOSE = 32;

        [DllImport("Ws2_32.dll")]
        internal static extern WsaEventHandle WSACreateEvent();

        [DllImport("Ws2_32.dll")]
        internal static extern int WSAEventSelect(
            IntPtr socket,
            WsaEventHandle hande,
            uint eventMask);

        [DllImport("Ws2_32.dll")]
        internal static extern bool WSASetEvent(WsaEventHandle hande);

        [DllImport("Ws2_32.dll")]
        internal static extern bool WSAResetEvent(WsaEventHandle hande);

        [DllImport("Ws2_32.dll")]
        internal static extern bool WSACloseEvent(IntPtr hande);

        [DllImport("Ws2_32.dll")]
        internal static extern uint WSAWaitForMultipleEvents(
            uint cEvents,
            IntPtr[] pEvents,
            bool fWaitAll,
            uint timeout,
            bool fAlterable);

        [DllImport("Ws2_32.dll")]
        internal static extern int WSAEnumNetworkEvents(
            IntPtr socket,
            WsaEventHandle eventHandle,
            ref WSANETWORKEVENTS eventInfo);

        [DllImport("Ws2_32.dll")]
        internal static extern int WSAGetLastError();


        [StructLayout(LayoutKind.Sequential)]
        internal struct WSANETWORKEVENTS
        {
            internal int lNetworkEvents;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
            internal int[] iErrorCode;
        }

        //---------------------------------------------------------------------
        // Utility functions.
        //---------------------------------------------------------------------

        internal static string? PtrToString(
            IntPtr stringPtr,
            int stringLength,
            Encoding encoding)
        {
            if (stringPtr == IntPtr.Zero || stringLength == 0)
            {
                return null;
            }

            var buffer = new byte[stringLength];
            Marshal.Copy(stringPtr, buffer, 0, stringLength);
            return encoding.GetString(buffer);
        }

        internal static T[] PtrToStructureArray<T>(
            IntPtr ptr,
            int count) where T : struct
        {
            var size = Marshal.SizeOf(typeof(T));
            var array = new T[count];

            for (var i = 0; i < count; i++)
            {
                var elementPtr = new IntPtr(ptr.ToInt64() + i * size);
                array[i] = Marshal.PtrToStructure<T>(elementPtr);
            }

            return array;
        }

        internal static void StructureArrayToPtr<T>(
            IntPtr ptr,
            T[] array) where T : struct
        {
            var size = Marshal.SizeOf(typeof(T));
            for (var i = 0; i < array.Length; i++)
            {
                var elementPtr = new IntPtr(ptr.ToInt64() + i * size);
                Marshal.StructureToPtr(array[i], elementPtr, false);
            }
        }
    }

    //-------------------------------------------------------------------------
    // Safe handles.
    //-------------------------------------------------------------------------

    /// <summary>
    /// Safe handle for a LIBSSH2_SESSION.
    /// </summary>
    internal class Libssh2SessionHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
#if DEBUG
        private readonly Thread owningThread = Thread.CurrentThread;
#endif

        private Libssh2SessionHandle() : base(true)
        {
            HandleTable.OnHandleCreated(this, "SSH session");
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            var result = (LIBSSH2_ERROR)NativeMethods.libssh2_session_free(
                this.handle);
            Debug.Assert(result == LIBSSH2_ERROR.NONE);

            HandleTable.OnHandleClosed(this);

            return true;
        }

        [Conditional("DEBUG")]
        internal void CheckCurrentThreadOwnsHandle()
        {
#if DEBUG
            Debug.Assert(Thread.CurrentThread == this.owningThread);
#endif
        }
    }

    /// <summary>
    /// Safe handle for a resource that's dependent on a LIBSSH2_SESSION.
    /// </summary>
    internal abstract class Libssh2SessionResourceHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
#if DEBUG
        private readonly Thread owningThread = Thread.CurrentThread;
#endif

        /// <summary>
        /// Handle to parent session.
        /// </summary>
        internal Libssh2SessionHandle? SessionHandle { get; private set; }

        protected Libssh2SessionResourceHandle() : base(true)
        {
            HandleTable.OnHandleCreated(this, "SSH session");
        }

        [Conditional("DEBUG")]
        internal void CheckCurrentThreadOwnsHandle()
        {
#if DEBUG
            Debug.Assert(Thread.CurrentThread == this.owningThread);
#endif
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            //
            // NB. Libssh2 manages channels and SFTPs as a sub-resource of a
            // session.
            //
            // Freeing a sub-resource is fine if the session is still around,
            // but trying to free a sub-resource *after* freeing a session will
            // cause an access violation.
            //
            // When calling dispose, it's therefore important to dispose
            // sub-resource before disposing their parent session - that's
            // what the following assertion is for.
            //
            // When handles are not disposed (for example, because the app
            // is being force-closed), then the finalizer may release
            // resources in arbitrary order. 
            //
            Invariant.ExpectNotNull(this.SessionHandle, nameof(this.SessionHandle));
            Debug.Assert(!this.SessionHandle!.IsClosed);

            CheckCurrentThreadOwnsHandle();

            if (this.SessionHandle.IsClosed)
            {
                // Do not free the channel.
                return false;
            }

            //
            // Release the actual handle.
            //
            ProtectedReleaseHandle();

            HandleTable.OnHandleClosed(this);

            return true;
        }

        internal void ValidateAndAttachToSession(Libssh2Session session)
        {
            Precondition.ExpectNotNull(session, nameof(session));

            if (this.IsInvalid)
            {
                throw session.CreateException(
                    (LIBSSH2_ERROR)NativeMethods.libssh2_session_last_errno(
                        session.Handle));
            }

            this.SessionHandle = session.Handle;
        }

        protected abstract void ProtectedReleaseHandle();
    }

    /// <summary>
    /// Safe handle for LIBSSH2_CHANNEL.
    /// </summary>
    internal class Libssh2ChannelHandle : Libssh2SessionResourceHandle
    {
        protected override void ProtectedReleaseHandle()
        {
            LIBSSH2_ERROR result;

            while ((result = (LIBSSH2_ERROR)
                NativeMethods.libssh2_channel_free(this.handle))
                == LIBSSH2_ERROR.EAGAIN)
            { };

            Debug.Assert(result == LIBSSH2_ERROR.NONE);
        }
    }

    /// <summary>
    /// Safe handle for LIBSSH2_SFTP.
    /// </summary>
    internal class Libssh2SftpChannelHandle : Libssh2SessionResourceHandle
    {
        protected override void ProtectedReleaseHandle()
        {
            LIBSSH2_ERROR result;

            while ((result = (LIBSSH2_ERROR)
                NativeMethods.libssh2_sftp_shutdown(this.handle))
                == LIBSSH2_ERROR.EAGAIN)
            { };

            Debug.Assert(result == LIBSSH2_ERROR.NONE);
        }
    }
    /// <summary>
    /// Safe handle for LIBSSH2_SFTP_HANDLE.
    /// </summary>
    internal class Libssh2SftpFileHandle : Libssh2SessionResourceHandle
    {
        protected override void ProtectedReleaseHandle()
        {
            LIBSSH2_ERROR result;

            while ((result = (LIBSSH2_ERROR)
                NativeMethods.libssh2_sftp_close_handle(this.handle))
                == LIBSSH2_ERROR.EAGAIN)
            { };

            Debug.Assert(result == LIBSSH2_ERROR.NONE);
        }
    }

    internal class WsaEventHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private WsaEventHandle() : base(true)
        {
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            var result = NativeMethods.WSACloseEvent(this.handle);
            Debug.Assert(result);
            return result;
        }
    }
}
