/*
 * xfer.c - file transfer and direct chat plugin for WeeChat
 *
 * Copyright (C) 2003-2021 Sébastien Helleu <flashcode@flashtux.org>
 *
 * This file is part of WeeChat, the extensible chat client.
 *
 * WeeChat is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * WeeChat is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WeeChat.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <netdb.h>
#include <resolv.h>
#include <gcrypt.h>
#include <arpa/inet.h>

#include "../weechat-plugin.h"
#include "xfer.h"
#include "xfer-buffer.h"
#include "xfer-command.h"
#include "xfer-completion.h"
#include "xfer-config.h"
#include "xfer-file.h"
#include "xfer-info.h"
#include "xfer-network.h"
#include "xfer-upgrade.h"


WEECHAT_PLUGIN_NAME(XFER_PLUGIN_NAME);
WEECHAT_PLUGIN_DESCRIPTION(N_("DCC file transfer and direct chat"));
WEECHAT_PLUGIN_AUTHOR("Sébastien Helleu <flashcode@flashtux.org>");
WEECHAT_PLUGIN_VERSION(WEECHAT_VERSION);
WEECHAT_PLUGIN_LICENSE(WEECHAT_LICENSE);
WEECHAT_PLUGIN_PRIORITY(7000);

struct t_weechat_plugin *weechat_xfer_plugin = NULL;

char *xfer_type_string[] =             /* strings for types                 */
{ "file_recv", "file_send", "chat_recv",
  "chat_send"
};

char *xfer_protocol_string[] =         /* strings for protocols             */
{ "none", "dcc"
};

char *xfer_status_string[] =           /* strings for status                */
{ N_("waiting"), N_("connecting"),
  N_("active"), N_("done"), N_("failed"),
  N_("aborted"), N_("hashing")
};

char *xfer_hash_status_string[] =      /* strings for hash status           */
{ "?",
  N_("CRC in progress"), N_("CRC OK"),
  N_("wrong CRC"), N_("CRC error")
};

struct t_xfer *xfer_list = NULL;       /* list of files/chats               */
struct t_xfer *last_xfer = NULL;       /* last file/chat in list            */
int xfer_count = 0;                    /* number of xfer                    */

int xfer_signal_upgrade_received = 0;  /* signal "upgrade" received ?       */

void xfer_disconnect_all ();


/*
 * Checks if a xfer pointer is valid.
 *
 * Returns:
 *   1: xfer exists
 *   0: xfer does not exist
 */

int
xfer_valid (struct t_xfer *xfer)
{
    struct t_xfer *ptr_xfer;

    if (!xfer)
        return 0;

    for (ptr_xfer = xfer_list; ptr_xfer;
         ptr_xfer = ptr_xfer->next_xfer)
    {
        if (ptr_xfer == xfer)
            return 1;
    }

    /* xfer not found */
    return 0;
}

/*
 * Callback for signal "upgrade".
 */

int
xfer_signal_upgrade_cb (const void *pointer, void *data,
                        const char *signal, const char *type_data,
                        void *signal_data)
{
    /* make C compiler happy */
    (void) pointer;
    (void) data;
    (void) signal;
    (void) type_data;

    xfer_signal_upgrade_received = 1;

    if (signal_data && (strcmp (signal_data, "quit") == 0))
        xfer_disconnect_all ();

    return WEECHAT_RC_OK;
}


/*
 * Creates directories for xfer plugin.
 */

void
xfer_create_directories ()
{
    char *path;
    struct t_hashtable *options;

    options = weechat_hashtable_new (
        32,
        WEECHAT_HASHTABLE_STRING,
        WEECHAT_HASHTABLE_STRING,
        NULL, NULL);
    if (options)
        weechat_hashtable_set (options, "directory", "data");

    /* create download directory */
    path = weechat_string_eval_path_home (
        weechat_config_string (xfer_config_file_download_path),
        NULL, NULL, options);
    if (path)
    {
        (void) weechat_mkdir_parents (path, 0700);
        free (path);
    }

    /* create upload directory */
    path = weechat_string_eval_path_home (
        weechat_config_string (xfer_config_file_upload_path),
        NULL, NULL, options);
    if (path)
    {
        (void) weechat_mkdir_parents (path, 0700);
        free (path);
    }

    if (options)
        weechat_hashtable_free (options);
}

/*
 * Searches for xfer type.
 *
 * Returns index of type in enum t_xfer_type, -1 if not found.
 */

int
xfer_search_type (const char *type)
{
    int i;

    for (i = 0; i < XFER_NUM_TYPES; i++)
    {
        if (weechat_strcasecmp (xfer_type_string[i], type) == 0)
            return i;
    }

    /* xfer type not found */
    return -1;
}

/*
 * Searches for xfer protocol.
 *
 * Returns index of protocol in enum t_xfer_protocol, -1 if not found.
 */

int
xfer_search_protocol (const char *protocol)
{
    int i;

    for (i = 0; i < XFER_NUM_PROTOCOLS; i++)
    {
        if (weechat_strcasecmp (xfer_protocol_string[i], protocol) == 0)
            return i;
    }

    /* xfer protocol not found */
    return -1;
}

/*
 * Searches for a xfer.
 *
 * Returns pointer to xfer found, NULL if not found.
 */

struct t_xfer *
xfer_search (const char *plugin_name, const char *plugin_id, enum t_xfer_type type,
             enum t_xfer_status status, int port)
{
    struct t_xfer *ptr_xfer;

    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        if ((weechat_strcasecmp (ptr_xfer->plugin_name, plugin_name) == 0)
            && (weechat_strcasecmp (ptr_xfer->plugin_id, plugin_id) == 0)
            && (ptr_xfer->type == type)
            && (ptr_xfer->status = status)
            && (ptr_xfer->port == port))
            return ptr_xfer;
    }

    /* xfer not found */
    return NULL;
}

/*
 * Searches for a xfer by number (first xfer is 0).
 *
 * Returns pointer to xfer found, NULL if not found.
 */

struct t_xfer *
xfer_search_by_number (int number)
{
    struct t_xfer *ptr_xfer;
    int i;

    i = 0;
    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        if (i == number)
            return ptr_xfer;
        i++;
    }

    /* xfer not found */
    return NULL;
}

/*
 * Searches for a xfer by buffer (for chat only).
 *
 * Returns pointer to xfer found, NULL if not found.
 */

struct t_xfer *
xfer_search_by_buffer (struct t_gui_buffer *buffer)
{
    struct t_xfer *ptr_xfer;

    if (!buffer)
        return NULL;

    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        if (ptr_xfer->buffer == buffer)
            return ptr_xfer;
    }

    /* xfer not found */
    return NULL;
}

/*
 * Closes a xfer.
 */

void
xfer_close (struct t_xfer *xfer, enum t_xfer_status status)
{
    struct stat st;

    xfer->status = status;

    if (XFER_HAS_ENDED(xfer->status))
    {
        if (xfer->hook_fd)
        {
            weechat_unhook (xfer->hook_fd);
            xfer->hook_fd = NULL;
        }
        if (xfer->hook_timer)
        {
            weechat_unhook (xfer->hook_timer);
            xfer->hook_timer = NULL;
        }
        if (xfer->hook_connect)
        {
            weechat_unhook (xfer->hook_connect);
            xfer->hook_connect = NULL;
        }
        if (XFER_IS_FILE(xfer->type))
        {
            weechat_printf (NULL,
                            _("%s%s: file %s %s %s (%s): %s"),
                            (xfer->status == XFER_STATUS_DONE) ? "" : weechat_prefix ("error"),
                            XFER_PLUGIN_NAME,
                            xfer->filename,
                            (xfer->type == XFER_TYPE_FILE_SEND) ? _("sent to") : _("received from"),
                            xfer->remote_nick,
                            xfer->remote_address_str,
                            (xfer->status == XFER_STATUS_DONE) ? _("OK") : _("FAILED"));
            xfer_network_child_kill (xfer);
        }
    }
    if (xfer->status == XFER_STATUS_ABORTED)
    {
        if (XFER_IS_CHAT(xfer->type))
        {
            weechat_printf (xfer->buffer,
                            _("%s%s: chat closed with %s "
                              "(%s)"),
                            weechat_prefix ("network"),
                            XFER_PLUGIN_NAME,
                            xfer->remote_nick,
                            xfer->remote_address_str);
        }
    }

    /* remove empty file if received file failed and nothing was transferred */
    if (((xfer->status == XFER_STATUS_FAILED)
         || (xfer->status == XFER_STATUS_ABORTED))
        && XFER_IS_FILE(xfer->type)
        && XFER_IS_RECV(xfer->type)
        && xfer->temp_local_filename
        && xfer->pos == 0)
    {
        /* erase file only if really empty on disk */
        if (stat (xfer->temp_local_filename, &st) != -1)
        {
            if ((unsigned long long) st.st_size == 0)
                unlink (xfer->temp_local_filename);
        }
    }

    /* rename received file if it has a suffix */
    if ((xfer->status == XFER_STATUS_DONE)
        && XFER_IS_FILE(xfer->type)
        && XFER_IS_RECV(xfer->type)
        && (strcmp (xfer->local_filename, xfer->temp_local_filename) != 0))
    {
        rename (xfer->temp_local_filename, xfer->local_filename);
    }

    if (XFER_IS_FILE(xfer->type))
        xfer_file_calculate_speed (xfer, 1);

    if (xfer->sock >= 0)
    {
        close (xfer->sock);
        xfer->sock = -1;
    }
    if (xfer->file >= 0)
    {
        close (xfer->file);
        xfer->file = -1;
    }

    if (XFER_HAS_ENDED(xfer->status))
        xfer_send_signal (xfer, "xfer_ended");
}

/*
 * Disconnects all active xfer (with a socket).
 */

void
xfer_disconnect_all ()
{
    struct t_xfer *ptr_xfer;

    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        if (ptr_xfer->sock >= 0)
        {
            if (ptr_xfer->status == XFER_STATUS_ACTIVE)
            {
                weechat_printf (NULL,
                                _("%s%s: aborting active xfer: \"%s\" from %s"),
                                weechat_prefix ("error"), XFER_PLUGIN_NAME,
                                ptr_xfer->filename, ptr_xfer->remote_nick);
                weechat_log_printf (_("%s%s: aborting active xfer: \"%s\" from %s"),
                                    "", XFER_PLUGIN_NAME,
                                    ptr_xfer->filename,
                                    ptr_xfer->remote_nick);
            }
            xfer_close (ptr_xfer, XFER_STATUS_FAILED);
        }
    }
}

/*
 * Checks if a port is in used.
 *
 * Returns:
 *   1: port is in used (by an active or connecting xfer)
 *   0: port is not in used
 */

int
xfer_port_in_use (int port)
{
    struct t_xfer *ptr_xfer;

    /* skip any currently used ports */
    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        if ((ptr_xfer->port == port) && (!XFER_HAS_ENDED(ptr_xfer->status)))
            return 1;
    }

    /* port not in use */
    return 0;
}

/*
 * Sends a signal for a xfer.
 */

void
xfer_send_signal (struct t_xfer *xfer, const char *signal)
{
    struct t_infolist *infolist;

    infolist = weechat_infolist_new ();
    if (infolist)
    {
        if (xfer_add_to_infolist (infolist, xfer))
        {
            (void) weechat_hook_signal_send (signal,
                                             WEECHAT_HOOK_SIGNAL_POINTER,
                                             infolist);
        }
        weechat_infolist_free (infolist);
    }
}

/*
 * Allocates a new xfer.
 *
 * Returns pointer to new xfer, NULL if error.
 */

struct t_xfer *
xfer_alloc ()
{
    struct t_xfer *new_xfer;
    time_t time_now;

    /* create new xfer struct */
    if ((new_xfer = malloc (sizeof (*new_xfer))) == NULL)
        return NULL;

    time_now = time (NULL);

    new_xfer->plugin_id = NULL;
    new_xfer->plugin_name = NULL;
    new_xfer->type = 0;
    new_xfer->protocol = 0;
    new_xfer->remote_nick = NULL;
    new_xfer->local_nick = NULL;
    new_xfer->charset_modifier = NULL;
    new_xfer->filename = NULL;
    new_xfer->size = 0;
    new_xfer->proxy = NULL;
    new_xfer->local_address = NULL;
    new_xfer->local_address_length = 0;
    new_xfer->local_address_str = NULL;
    new_xfer->remote_address = NULL;
    new_xfer->remote_address_length = 0;
    new_xfer->remote_address_str = NULL;
    new_xfer->port = 0;
    new_xfer->status = 0;
    new_xfer->buffer = NULL;
    new_xfer->remote_nick_color = NULL;
    new_xfer->fast_send = weechat_config_boolean (xfer_config_network_fast_send);
    new_xfer->send_ack = weechat_config_boolean (xfer_config_network_send_ack);
    new_xfer->blocksize = weechat_config_integer (xfer_config_network_blocksize);
    new_xfer->start_time = time_now;
    new_xfer->start_transfer = time_now;
    new_xfer->sock = -1;
    new_xfer->child_pid = 0;
    new_xfer->child_read = -1;
    new_xfer->child_write = -1;
    new_xfer->hook_fd = NULL;
    new_xfer->hook_timer = NULL;
    new_xfer->hook_connect = NULL;
    new_xfer->unterminated_message = NULL;
    new_xfer->file = -1;
    new_xfer->local_filename = NULL;
    new_xfer->temp_local_filename = NULL;
    new_xfer->filename_suffix = -1;
    new_xfer->pos = 0;
    new_xfer->ack = 0;
    new_xfer->start_resume = 0;
    new_xfer->last_check_time = time_now;
    new_xfer->last_check_pos = time_now;
    new_xfer->last_activity = 0;
    new_xfer->bytes_per_sec = 0;
    new_xfer->eta = 0;
    new_xfer->hash_handle = NULL;
    new_xfer->hash_target = NULL;
    new_xfer->hash_status = XFER_HASH_STATUS_UNKNOWN;

    new_xfer->prev_xfer = NULL;
    new_xfer->next_xfer = xfer_list;
    if (xfer_list)
        xfer_list->prev_xfer = new_xfer;
    else
        last_xfer = new_xfer;
    xfer_list = new_xfer;

    xfer_count++;

    return new_xfer;
}

/*
 * Checks if the given server/nick is auto-accepted.
 *
 * Returns:
 *   1: nick auto-accepted
 *   0: nick not auto-accepted
 */

int
xfer_nick_auto_accepted (const char *server, const char *nick)
{
    int rc, num_nicks, i;
    char **nicks, *pos;

    rc = 0;

    nicks = weechat_string_split (
        weechat_config_string (xfer_config_file_auto_accept_nicks),
        ",",
        NULL,
        WEECHAT_STRING_SPLIT_STRIP_LEFT
        | WEECHAT_STRING_SPLIT_STRIP_RIGHT
        | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS,
        0,
        &num_nicks);
    if (nicks)
    {
        for (i = 0; i < num_nicks; i++)
        {
            pos = strrchr (nicks[i], '.');
            if (pos)
            {
                if ((weechat_strncasecmp (server, nicks[i], pos - nicks[i]) == 0)
                    && (weechat_strcasecmp (nick, pos + 1) == 0))
                {
                    rc = 1;
                    break;
                }
            }
            else
            {
                if (weechat_strcasecmp (nick, nicks[i]) == 0)
                {
                    rc = 1;
                    break;
                }
            }
        }
        weechat_string_free_split (nicks);
    }

    return rc;
}

/*
 * Searches CRC32 in a filename.
 *
 * If more than one CRC32 are found, the last one is returned
 * (with the higher index in filename).
 *
 * The chars before/after CRC32 must be either beginning/end of string or
 * non-hexadecimal chars.
 *
 * Examples:
 *
 *   test_filename     => -1 (not found: no CRC32)
 *   test_1234abcd     => 5  ("1234abcd")
 *   1234abcd_test     => 0  ("1234abcd")
 *   1234abcd_12345678 => 9  ("12345678")
 *   123456789abcdef   => -1 (not found: missing delimiter around CRC32)
 *
 * Returns pointer to last CRC32 in string, NULL if no CRC32 was found.
 */

const char *
xfer_filename_crc32 (const char *filename)
{
    int length;
    const char *ptr_string, *ptr_crc32;

    length = 0;
    ptr_crc32 = NULL;

    ptr_string = filename;
    while (ptr_string && ptr_string[0])
    {
        if (((ptr_string[0] >= '0') && (ptr_string[0] <= '9'))
            || ((ptr_string[0] >= 'A') && (ptr_string[0] <= 'F'))
            || ((ptr_string[0] >= 'a') && (ptr_string[0] <= 'f')))
        {
            length++;
        }
        else
        {
            if (length == 8)
                ptr_crc32 = ptr_string - 8;
            length = 0;
        }

        ptr_string = weechat_utf8_next_char (ptr_string);
    }
    if (length == 8)
        ptr_crc32 = ptr_string - 8;

    return ptr_crc32;
}

/*
 * Adds a xfer to list.
 *
 * Returns pointer to new xfer, NULL if error.
 */

struct t_xfer *
xfer_new (const char *plugin_name, const char *plugin_id,
          enum t_xfer_type type, enum t_xfer_protocol protocol,
          const char *remote_nick, const char *local_nick,
          const char *charset_modifier, const char *filename,
          unsigned long long size, const char *proxy, struct sockaddr *address,
          socklen_t address_length, int port, int sock,
          const char *local_filename)
{
    struct t_xfer *new_xfer;
    const char *ptr_crc32;
    char str_address[NI_MAXHOST], *color;
    int rc;

    new_xfer = xfer_alloc ();
    if (!new_xfer)
    {
        weechat_printf (NULL,
                        _("%s%s: not enough memory for new xfer"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME);
        return NULL;
    }

    if (!xfer_buffer
        && weechat_config_boolean (xfer_config_look_auto_open_buffer))
    {
        xfer_buffer_open ();
    }

    /* initialize new xfer */
    new_xfer->plugin_name = strdup (plugin_name);
    new_xfer->plugin_id = strdup (plugin_id);
    new_xfer->type = type;
    new_xfer->protocol = protocol;
    new_xfer->remote_nick = strdup (remote_nick);
    color = weechat_info_get ("irc_nick_color_name", remote_nick);
    new_xfer->remote_nick_color = (color) ? strdup (color) : NULL;
    if (color)
        free (color);
    new_xfer->local_nick = (local_nick) ? strdup (local_nick) : NULL;
    new_xfer->charset_modifier = (charset_modifier) ? strdup (charset_modifier) : NULL;
    if (XFER_IS_FILE(type))
        new_xfer->filename = (filename) ? strdup (filename) : NULL;
    else
        new_xfer->filename = strdup (_("xfer chat"));
    new_xfer->size = size;
    new_xfer->proxy = (proxy) ? strdup (proxy) : NULL;
    new_xfer->port = port;

    rc = getnameinfo ((struct sockaddr *)address, address_length, str_address,
                      sizeof (str_address), NULL, 0, NI_NUMERICHOST);
    if (rc != 0)
    {
        weechat_printf (NULL,
                        _("%s%s: unable to interpret address: error %d %s"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        rc, gai_strerror (rc));
        snprintf (str_address, sizeof (str_address), "?");
    }

    if (XFER_IS_RECV(type))
    {
        new_xfer->local_address_str = strdup ("");
        xfer_set_remote_address (new_xfer, address, address_length, str_address);
    }
    else
    {
        xfer_set_local_address (new_xfer, address, address_length, str_address);
        new_xfer->remote_address_str = strdup ("");
    }

    new_xfer->status = XFER_STATUS_WAITING;
    new_xfer->sock = sock;
    if (local_filename)
        new_xfer->local_filename = strdup (local_filename);
    else
        xfer_file_find_filename (new_xfer);

    new_xfer->hash_handle = NULL;
    new_xfer->hash_target = NULL;
    new_xfer->hash_status = XFER_HASH_STATUS_UNKNOWN;

    if ((type == XFER_TYPE_FILE_RECV)
        && weechat_config_boolean (xfer_config_file_auto_check_crc32))
    {
        ptr_crc32 = xfer_filename_crc32 (new_xfer->filename);
        if (ptr_crc32)
        {
            new_xfer->hash_handle = malloc (sizeof (gcry_md_hd_t));
            if (new_xfer->hash_handle)
            {
                if (gcry_md_open (new_xfer->hash_handle, GCRY_MD_CRC32, 0) == 0)
                {
                    new_xfer->hash_target = weechat_strndup (ptr_crc32, 8);
                    new_xfer->hash_status = XFER_HASH_STATUS_IN_PROGRESS;
                }
                else
                {
                    free (new_xfer->hash_handle);
                    new_xfer->hash_handle = NULL;
                    weechat_printf (NULL,
                                    _("%s%s: hashing error"),
                                    weechat_prefix ("error"), XFER_PLUGIN_NAME);
                }
            }
        }
    }

    /* write info message on core buffer */
    switch (type)
    {
        case XFER_TYPE_FILE_RECV:
            weechat_printf (NULL,
                            _("%s: incoming file from %s "
                              "(%s, %s.%s), name: %s, %llu bytes "
                              "(protocol: %s)"),
                            XFER_PLUGIN_NAME,
                            remote_nick,
                            str_address,
                            plugin_name,
                            plugin_id,
                            filename,
                            size,
                            xfer_protocol_string[protocol]);
            xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
            break;
        case XFER_TYPE_FILE_SEND:
            weechat_printf (NULL,
                            _("%s: offering file to %s (%s.%s), name: %s "
                              "(local filename: %s), %llu bytes (protocol: %s)"),
                            XFER_PLUGIN_NAME,
                            remote_nick,
                            plugin_name,
                            plugin_id,
                            filename,
                            local_filename,
                            size,
                            xfer_protocol_string[protocol]);
            xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
            break;
        case XFER_TYPE_CHAT_RECV:
            weechat_printf (NULL,
                            _("%s: incoming chat request from %s "
                              "(%s, %s.%s)"),
                            XFER_PLUGIN_NAME,
                            remote_nick,
                            str_address,
                            plugin_name,
                            plugin_id);
            xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
            break;
        case XFER_TYPE_CHAT_SEND:
            weechat_printf (NULL,
                            _("%s: sending chat request to %s (%s.%s)"),
                            XFER_PLUGIN_NAME,
                            remote_nick,
                            plugin_name,
                            plugin_id);
            xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
            break;
        case XFER_NUM_TYPES:
            break;
    }

    if (XFER_IS_FILE(type) && (!new_xfer->local_filename))
    {
        xfer_close (new_xfer, XFER_STATUS_FAILED);
        xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
        return NULL;
    }

    if (XFER_IS_FILE(type) && (new_xfer->start_resume > 0))
    {
        weechat_printf (NULL,
                        _("%s: file %s (local filename: %s) "
                          "will be resumed at position %llu"),
                        XFER_PLUGIN_NAME,
                        new_xfer->filename,
                        new_xfer->local_filename,
                        new_xfer->start_resume);
        xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
    }

    /* connect if needed and display again xfer buffer */
    if (XFER_IS_SEND(type))
    {
        if (!xfer_network_connect (new_xfer))
        {
            xfer_close (new_xfer, XFER_STATUS_FAILED);
            xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
            return NULL;
        }
    }

    /*
     * auto-accept file/chat if nick is auto-accepted, or if file/chat is
     * auto-accepted
     */
    if ((XFER_IS_RECV(type)
         && xfer_nick_auto_accepted (new_xfer->plugin_id, new_xfer->remote_nick))
        || ((type == XFER_TYPE_FILE_RECV)
            && weechat_config_boolean (xfer_config_file_auto_accept_files))
        || ((type == XFER_TYPE_CHAT_RECV)
            && weechat_config_boolean (xfer_config_file_auto_accept_chats)))
    {
        xfer_network_accept (new_xfer);
    }
    else
    {
        xfer_buffer_refresh (WEECHAT_HOTLIST_PRIVATE);
    }

    return new_xfer;
}

/*
 * Sets the remote address field.
 */

void
xfer_set_remote_address (struct t_xfer *xfer, const struct sockaddr *address,
                         socklen_t length, const char *address_str)
{
    if (xfer->remote_address)
        free (xfer->remote_address);
    xfer->remote_address = malloc (length);
    xfer->remote_address_length = length;
    if (xfer->remote_address)
	memcpy (xfer->remote_address, address, length);

    if (xfer->remote_address_str)
        free (xfer->remote_address_str);
    xfer->remote_address_str = strdup ((address_str) ? address_str : "");
}

/*
 * Sets the local address field.
 */
void
xfer_set_local_address (struct t_xfer *xfer, const struct sockaddr *address,
                         socklen_t length, const char *address_str)
{
    if (xfer->local_address)
        free (xfer->local_address);
    xfer->local_address = malloc (length);
    xfer->local_address_length = length;
    if (xfer->local_address)
	memcpy (xfer->local_address, address, length);

    if (xfer->local_address_str)
        free (xfer->local_address_str);
    xfer->local_address_str = strdup ((address_str) ? address_str : "");
}

/*
 * Frees xfer struct and removes it from list.
 */

void
xfer_free (struct t_xfer *xfer)
{
    struct t_xfer *new_xfer_list;

    if (!xfer)
        return;

    /* remove xfer from list */
    if (last_xfer == xfer)
        last_xfer = xfer->prev_xfer;
    if (xfer->prev_xfer)
    {
        (xfer->prev_xfer)->next_xfer = xfer->next_xfer;
        new_xfer_list = xfer_list;
    }
    else
        new_xfer_list = xfer->next_xfer;
    if (xfer->next_xfer)
        (xfer->next_xfer)->prev_xfer = xfer->prev_xfer;

    /* free data */
    if (xfer->plugin_id)
        free (xfer->plugin_id);
    if (xfer->plugin_name)
        free (xfer->plugin_name);
    if (xfer->remote_nick)
        free (xfer->remote_nick);
    if (xfer->local_nick)
        free (xfer->local_nick);
    if (xfer->charset_modifier)
        free (xfer->charset_modifier);
    if (xfer->filename)
        free (xfer->filename);
    if (xfer->proxy)
        free (xfer->proxy);
    if (xfer->local_address)
        free (xfer->local_address);
    if (xfer->local_address_str)
        free (xfer->local_address_str);
    if (xfer->remote_address)
        free (xfer->remote_address);
    if (xfer->remote_address_str)
        free (xfer->remote_address_str);
    if (xfer->buffer)
        free (xfer->buffer);
    if (xfer->remote_nick_color)
        free (xfer->remote_nick_color);
    if (xfer->hook_fd)
        weechat_unhook (xfer->hook_fd);
    if (xfer->hook_timer)
        weechat_unhook (xfer->hook_timer);
    if (xfer->hook_connect)
        weechat_unhook (xfer->hook_connect);
    if (xfer->unterminated_message)
        free (xfer->unterminated_message);
    if (xfer->local_filename)
        free (xfer->local_filename);
    if (xfer->temp_local_filename)
        free (xfer->temp_local_filename);
    if (xfer->hash_handle)
    {
        gcry_md_close (*xfer->hash_handle);
        free (xfer->hash_handle);
    }
    if (xfer->hash_target)
        free (xfer->hash_target);

    free (xfer);

    xfer_list = new_xfer_list;

    xfer_count--;
    if (xfer_buffer_selected_line >= xfer_count)
        xfer_buffer_selected_line = (xfer_count == 0) ? 0 : xfer_count - 1;
}

/*
 * Frees all xfers.
 */

void
xfer_free_all ()
{
    while (xfer_list)
    {
        xfer_free (xfer_list);
    }
}

/*
 * Resolves address.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
xfer_resolve_addr (const char *str_address, const char *str_port,
                   struct sockaddr *addr, socklen_t *addr_len, int ai_flags)
{
    struct addrinfo *ainfo, hints;
    int rc;

    memset (&hints, 0, sizeof (struct addrinfo));
    hints.ai_flags = ai_flags;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = 0;
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;

    res_init ();
    rc = getaddrinfo (str_address, str_port, &hints, &ainfo);
    if ((rc == 0) && ainfo && ainfo->ai_addr)
    {
        if (ainfo->ai_addrlen > *addr_len)
        {
            weechat_printf (NULL,
                            _("%s%s: address \"%s\" resolved to a larger "
                              "sockaddr than expected"),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME,
                            str_address);
            freeaddrinfo (ainfo);
            return 0;
        }
        memcpy (addr, ainfo->ai_addr, ainfo->ai_addrlen);
        *addr_len = ainfo->ai_addrlen;
        freeaddrinfo (ainfo);
        return 1;
    }

    weechat_printf (NULL,
                    _("%s%s: invalid address \"%s\": error %d %s"),
                    weechat_prefix ("error"), XFER_PLUGIN_NAME,
                    str_address, rc, gai_strerror (rc));
    if ((rc == 0) && ainfo)
        freeaddrinfo (ainfo);
    return 0;
}

/*
 * Callback for signal "xfer_add".
 */

int
xfer_add_cb (const void *pointer, void *data,
             const char *signal, const char *type_data,
             void *signal_data)
{
    struct t_infolist *infolist;
    const char *plugin_name, *plugin_id, *str_type, *str_protocol;
    const char *remote_nick, *local_nick, *charset_modifier, *filename, *proxy;
    const char *str_address, *str_port;
    int type, protocol, args, port_start, port_end, sock, port;
    char *path, *filename2, *short_filename, *pos, str_port_temp[16];
    struct stat st;
    struct sockaddr_storage addr, own_ip_addr, bind_addr;
    struct sockaddr *out_addr = (struct sockaddr*)&addr;
    socklen_t length, bind_addr_len;
    unsigned long long file_size;
    struct t_xfer *ptr_xfer;
    struct t_hashtable *options;

    /* make C compiler happy */
    (void) pointer;
    (void) data;
    (void) signal;
    (void) type_data;

    if (!signal_data)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, "xfer_add");
        return WEECHAT_RC_ERROR;
    }

    infolist = (struct t_infolist *)signal_data;

    if (!weechat_infolist_next (infolist))
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, "xfer_add");
        return WEECHAT_RC_ERROR;
    }

    filename2 = NULL;
    short_filename = NULL;
    file_size = 0;

    sock = -1;

    plugin_name = weechat_infolist_string (infolist, "plugin_name");
    plugin_id = weechat_infolist_string (infolist, "plugin_id");
    str_type = weechat_infolist_string (infolist, "type_string");
    str_protocol = weechat_infolist_string (infolist, "protocol_string");
    remote_nick = weechat_infolist_string (infolist, "remote_nick");
    local_nick = weechat_infolist_string (infolist, "local_nick");
    charset_modifier = weechat_infolist_string (infolist, "charset_modifier");
    filename = weechat_infolist_string (infolist, "filename");
    proxy = weechat_infolist_string (infolist, "proxy");
    protocol = XFER_NO_PROTOCOL;

    if (!plugin_name || !plugin_id || !str_type || !remote_nick || !local_nick)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, "xfer_add");
        goto error;
    }

    type = xfer_search_type (str_type);
    if (type < 0)
    {
        weechat_printf (NULL,
                        _("%s%s: unknown xfer type \"%s\""),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, str_type);
        goto error;
    }

    if (XFER_IS_FILE(type) && (!filename || !str_protocol))
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, "xfer_add");
        goto error;
    }

    if (XFER_IS_FILE(type))
    {
        protocol = xfer_search_protocol (str_protocol);
        if (protocol < 0)
        {
            weechat_printf (NULL,
                            _("%s%s: unknown xfer protocol \"%s\""),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME,
                            str_protocol);
            goto error;
        }
    }

    if (type == XFER_TYPE_FILE_RECV)
    {
        filename2 = strdup (filename);
        sscanf (weechat_infolist_string (infolist, "size"), "%llu", &file_size);
    }

    if (type == XFER_TYPE_FILE_SEND)
    {
        /* add home if filename not beginning with '/' or '~' (not for Win32) */
#ifdef _WIN32
        filename2 = strdup (filename);
#else
        if (filename[0] == '/')
            filename2 = strdup (filename);
        else if (filename[0] == '~')
            filename2 = weechat_string_expand_home (filename);
        else
        {
            options = weechat_hashtable_new (
                32,
                WEECHAT_HASHTABLE_STRING,
                WEECHAT_HASHTABLE_STRING,
                NULL, NULL);
            if (options)
                weechat_hashtable_set (options, "directory", "data");
            path = weechat_string_eval_path_home (
                weechat_config_string (xfer_config_file_upload_path),
                NULL, NULL, options);
            if (options)
                weechat_hashtable_free (options);
            if (!path)
            {
                weechat_printf (NULL,
                                _("%s%s: not enough memory (%s)"),
                                weechat_prefix ("error"), XFER_PLUGIN_NAME,
                                "xfer_add, path");
                goto error;
            }
            filename2 = malloc (strlen (path) + strlen (filename) + 4);
            if (!filename2)
            {
                weechat_printf (NULL,
                                _("%s%s: not enough memory (%s)"),
                                weechat_prefix ("error"), XFER_PLUGIN_NAME,
                                "xfer_add, filename2");
                free (path);
                goto error;
            }
            strcpy (filename2, path);
            if (filename2[strlen (filename2) - 1] != DIR_SEPARATOR_CHAR)
                strcat (filename2, DIR_SEPARATOR);
            strcat (filename2, filename);
            free (path);
        }
#endif /* _WIN32 */
        if (!filename2)
        {
            weechat_printf (NULL,
                            _("%s%s: not enough memory (%s)"),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME,
                            "xfer_add, filename2");
            goto error;
        }
        /* check if file exists */
        if (stat (filename2, &st) == -1)
        {
            weechat_printf (NULL,
                            _("%s%s: cannot access file \"%s\""),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME,
                            filename2);
            goto error;
        }
        file_size = st.st_size;
    }
    port = weechat_infolist_integer (infolist, "port");

    /* resolve address */
    if (XFER_IS_RECV(type))
    {
        str_address = weechat_infolist_string (infolist, "remote_address");
        snprintf (str_port_temp, sizeof (str_port_temp), "%d", port);
        str_port = str_port_temp;
        length = sizeof (addr);
        if (!xfer_resolve_addr (str_address, str_port,
                                (struct sockaddr*)&addr, &length,
                                AI_NUMERICSERV | AI_NUMERICHOST))
        {
            goto error;
        }
    }
    else
    {
        memset (&bind_addr, 0, sizeof (bind_addr));

        /* determine bind_addr family from either own_ip or default */
        if (weechat_config_string (xfer_config_network_own_ip)
            && weechat_config_string (xfer_config_network_own_ip)[0])
        {
            /* resolve own_ip to a numeric address */
            str_address = weechat_config_string (xfer_config_network_own_ip);
            length = sizeof (own_ip_addr);

            if (!xfer_resolve_addr (str_address, NULL,
                                    (struct sockaddr*)&own_ip_addr, &length,
                                    AI_NUMERICSERV))
            {
                goto error;
            }

            /* set the advertised address to own_ip */
            out_addr = (struct sockaddr*)&own_ip_addr;

            /* bind_addr's family should be the advertised family */
            bind_addr.ss_family = own_ip_addr.ss_family;
        }
        else
        {
            /* no own_ip, so bind_addr's family comes from irc connection  */
            str_address = weechat_infolist_string (infolist, "local_address");
            length = sizeof (addr);
            if (!xfer_resolve_addr (str_address, NULL,
                                    (struct sockaddr*)&addr, &length,
                                    AI_NUMERICSERV | AI_NUMERICHOST))
            {
                goto error;
            }
            bind_addr.ss_family = addr.ss_family;
        }

        /* determine bind wildcard address */
        if (bind_addr.ss_family == AF_INET)
        {
            ((struct sockaddr_in*)&bind_addr)->sin_addr.s_addr = INADDR_ANY;
            bind_addr_len = sizeof (struct sockaddr_in);
        }
        else
        {
            memcpy (&((struct sockaddr_in6*)&bind_addr)->sin6_addr,
                    &in6addr_any, sizeof (in6addr_any));
            bind_addr_len = sizeof (struct sockaddr_in6);
        }

        /* open socket for xfer */
        sock = socket (bind_addr.ss_family, SOCK_STREAM, 0);
        if (sock < 0)
        {
            weechat_printf (NULL,
                            _("%s%s: cannot create socket for xfer: error %d %s"),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME,
                            errno, strerror (errno));
            goto error;
        }

        /* look for port */
        port = 0;

        if (weechat_config_string (xfer_config_network_port_range)
            && weechat_config_string (xfer_config_network_port_range)[0])
        {
            /* find a free port in the specified range */
            args = sscanf (weechat_config_string (xfer_config_network_port_range),
                           "%d-%d", &port_start, &port_end);
            if (args > 0)
            {
                port = port_start;
                if (args == 1)
                    port_end = port_start;

                /* loop through the entire allowed port range */
                while (port <= port_end)
                {
                    if (!xfer_port_in_use (port))
                    {
                        /* attempt to bind to the free port */
                        if (bind_addr.ss_family == AF_INET)
                            ((struct sockaddr_in *)&bind_addr)->sin_port = htons (port);
                        else
                            ((struct sockaddr_in6 *)&bind_addr)->sin6_port = htons (port);
                        if (bind (sock, (struct sockaddr *)&bind_addr, bind_addr_len) == 0)
                            break;
                    }
                    port++;
                }

                if (port > port_end)
                    port = -1;
            }
        }

        if (port == 0)
        {
            /* find port automatically */
            if (bind (sock, (struct sockaddr *)&bind_addr, bind_addr_len) == 0)
            {
                getsockname (sock, (struct sockaddr *)&bind_addr, &bind_addr_len);
                if (bind_addr.ss_family == AF_INET)
                    port = ntohs (((struct sockaddr_in *)&bind_addr)->sin_port);
                else
                    port = ntohs (((struct sockaddr_in6 *)&bind_addr)->sin6_port);
            }
            else
                port = -1;
        }

        if (port == -1)
        {
            /* Could not find any port to bind */
            weechat_printf (NULL,
                            _("%s%s: cannot find available port for xfer"),
                            weechat_prefix ("error"), XFER_PLUGIN_NAME);
            close (sock);
            goto error;
        }
    }

    if (XFER_IS_FILE(type))
    {
        /* extract short filename (without path) */
        pos = strrchr (filename2, DIR_SEPARATOR_CHAR);
        if (pos)
            short_filename = strdup (pos + 1);
        else
            short_filename = strdup (filename2);

        /* convert spaces to underscore if asked and needed */
        pos = short_filename;
        while (pos[0])
        {
            if (pos[0] == ' ')
            {
                if (weechat_config_boolean (xfer_config_file_convert_spaces))
                    pos[0] = '_';
            }
            pos++;
        }
    }

    if (type == XFER_TYPE_FILE_RECV)
    {
        if (filename2)
        {
            free (filename2);
            filename2 = NULL;
        }
    }

    /* add xfer entry and listen to socket if type is file or chat "send" */
    if (XFER_IS_FILE(type))
    {
        ptr_xfer = xfer_new (plugin_name, plugin_id, type, protocol,
                             remote_nick, local_nick, charset_modifier,
                             short_filename, file_size, proxy,
                             out_addr, length, port, sock, filename2);
    }
    else
    {
        ptr_xfer = xfer_new (plugin_name, plugin_id, type, protocol,
                             remote_nick, local_nick, charset_modifier, NULL,
                             0, proxy, out_addr, length, port, sock, NULL);
    }

    if (!ptr_xfer)
    {
        weechat_printf (NULL,
                        _("%s%s: error creating xfer"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME);
        close (sock);
        goto error;
    }

    /* send signal if type is file or chat "send" */
    if (XFER_IS_SEND(ptr_xfer->type) && !XFER_HAS_ENDED(ptr_xfer->status))
        xfer_send_signal (ptr_xfer, "xfer_send_ready");

    if (filename2)
        free (filename2);
    if (short_filename)
        free (short_filename);
    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_OK;

error:
    if (filename2)
        free (filename2);
    if (short_filename)
        free (short_filename);
    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_ERROR;
}

/*
 * Callback called when resume is accepted by sender.
 */

int
xfer_start_resume_cb (const void *pointer, void *data,
                      const char *signal, const char *type_data,
                      void *signal_data)
{
    struct t_infolist *infolist;
    struct t_xfer *ptr_xfer;
    const char *plugin_name, *plugin_id, *filename, *str_start_resume;
    int port;
    unsigned long long start_resume;

    /* make C compiler happy */
    (void) pointer;
    (void) data;
    (void) signal;
    (void) type_data;

    if (!signal_data)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_start_resume");
        return WEECHAT_RC_ERROR;
    }

    infolist = (struct t_infolist *)signal_data;

    if (!weechat_infolist_next (infolist))
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_start_resume");
        return WEECHAT_RC_ERROR;
    }

    plugin_name = weechat_infolist_string (infolist, "plugin_name");
    plugin_id = weechat_infolist_string (infolist, "plugin_id");
    filename = weechat_infolist_string (infolist, "filename");
    port = weechat_infolist_integer (infolist, "port");
    str_start_resume = weechat_infolist_string (infolist, "start_resume");

    if (!plugin_name || !plugin_id || !filename || !str_start_resume)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_start_resume");
        goto error;
    }

    sscanf (str_start_resume, "%llu", &start_resume);

    ptr_xfer = xfer_search (plugin_name, plugin_id, XFER_TYPE_FILE_RECV,
                            XFER_STATUS_CONNECTING, port);
    if (ptr_xfer)
    {
        ptr_xfer->pos = start_resume;
        ptr_xfer->ack = start_resume;
        ptr_xfer->start_resume = start_resume;
        ptr_xfer->last_check_pos = start_resume;
        xfer_network_connect_init (ptr_xfer);
    }
    else
    {
        weechat_printf (NULL,
                        _("%s%s: unable to resume file \"%s\" (port: %d, "
                          "start position: %llu): xfer not found or not ready "
                          "for transfer"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, filename,
                        port, start_resume);
    }

    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_OK;

error:
    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_ERROR;
}

/*
 * Callback called when sender receives resume request from receiver.
 */

int
xfer_accept_resume_cb (const void *pointer, void *data,
                       const char *signal, const char *type_data,
                       void *signal_data)
{
    struct t_infolist *infolist;
    struct t_xfer *ptr_xfer;
    const char *plugin_name, *plugin_id, *filename, *str_start_resume;
    int port;
    unsigned long long start_resume;

    /* make C compiler happy */
    (void) pointer;
    (void) data;
    (void) signal;
    (void) type_data;

    if (!signal_data)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_accept_resume");
        return WEECHAT_RC_ERROR;
    }

    infolist = (struct t_infolist *)signal_data;

    if (!weechat_infolist_next (infolist))
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_accept_resume");
        return WEECHAT_RC_ERROR;
    }

    plugin_name = weechat_infolist_string (infolist, "plugin_name");
    plugin_id = weechat_infolist_string (infolist, "plugin_id");
    filename = weechat_infolist_string (infolist, "filename");
    port = weechat_infolist_integer (infolist, "port");
    str_start_resume = weechat_infolist_string (infolist, "start_resume");

    if (!plugin_name || !plugin_id || !filename || !str_start_resume)
    {
        weechat_printf (NULL,
                        _("%s%s: missing arguments (%s)"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME,
                        "xfer_accept_resume");
        goto error;
    }

    sscanf (str_start_resume, "%llu", &start_resume);

    ptr_xfer = xfer_search (plugin_name, plugin_id, XFER_TYPE_FILE_SEND,
                            XFER_STATUS_CONNECTING, port);
    if (ptr_xfer)
    {
        ptr_xfer->pos = start_resume;
        ptr_xfer->ack = start_resume;
        ptr_xfer->start_resume = start_resume;
        ptr_xfer->last_check_pos = start_resume;
        xfer_send_signal (ptr_xfer, "xfer_send_accept_resume");

        weechat_printf (NULL,
                        _("%s: file %s resumed at position %llu"),
                        XFER_PLUGIN_NAME,
                        ptr_xfer->filename,
                        ptr_xfer->start_resume);
        xfer_buffer_refresh (WEECHAT_HOTLIST_MESSAGE);
    }
    else
    {
        weechat_printf (NULL,
                        _("%s%s: unable to accept resume file \"%s\" (port: %d, "
                          "start position: %llu): xfer not found or not ready "
                          "for transfer"),
                        weechat_prefix ("error"), XFER_PLUGIN_NAME, filename,
                        port, start_resume);
    }

    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_OK;

error:
    weechat_infolist_reset_item_cursor (infolist);
    return WEECHAT_RC_ERROR;
}

/*
 * Adds a xfer in an infolist.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
xfer_add_to_infolist (struct t_infolist *infolist, struct t_xfer *xfer)
{
    struct t_infolist_item *ptr_item;
    char value[128];

    if (!infolist || !xfer)
        return 0;

    ptr_item = weechat_infolist_new_item (infolist);
    if (!ptr_item)
        return 0;

    if (!weechat_infolist_new_var_string (ptr_item, "plugin_name", xfer->plugin_name))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "plugin_id", xfer->plugin_id))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "type", xfer->type))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "type_string", xfer_type_string[xfer->type]))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "protocol", xfer->protocol))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "protocol_string", xfer_protocol_string[xfer->protocol]))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "remote_nick", xfer->remote_nick))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "local_nick", xfer->local_nick))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "charset_modifier", xfer->charset_modifier))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "filename", xfer->filename))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->size);
    if (!weechat_infolist_new_var_string (ptr_item, "size", value))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "proxy", xfer->proxy))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "local_address", xfer->local_address_str))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "remote_address", xfer->remote_address_str))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "port", xfer->port))
        return 0;

    if (!weechat_infolist_new_var_integer (ptr_item, "status", xfer->status))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "status_string", xfer_status_string[xfer->status]))
        return 0;
    if (!weechat_infolist_new_var_pointer (ptr_item, "buffer", xfer->buffer))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "remote_nick_color", xfer->remote_nick_color))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "fast_send", xfer->fast_send))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "blocksize", xfer->blocksize))
        return 0;
    if (!weechat_infolist_new_var_time (ptr_item, "start_time", xfer->start_time))
        return 0;
    if (!weechat_infolist_new_var_time (ptr_item, "start_transfer", xfer->start_transfer))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "sock", xfer->sock))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "child_pid", xfer->child_pid))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "child_read", xfer->child_read))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "child_write", xfer->child_write))
        return 0;
    if (!weechat_infolist_new_var_pointer (ptr_item, "hook_fd", xfer->hook_fd))
        return 0;
    if (!weechat_infolist_new_var_pointer (ptr_item, "hook_timer", xfer->hook_timer))
        return 0;
    if (!weechat_infolist_new_var_pointer (ptr_item, "hook_connect", xfer->hook_connect))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "unterminated_message", xfer->unterminated_message))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "file", xfer->file))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "local_filename", xfer->local_filename))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "temp_local_filename", xfer->temp_local_filename))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "filename_suffix", xfer->filename_suffix))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->pos);
    if (!weechat_infolist_new_var_string (ptr_item, "pos", value))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->ack);
    if (!weechat_infolist_new_var_string (ptr_item, "ack", value))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->start_resume);
    if (!weechat_infolist_new_var_string (ptr_item, "start_resume", value))
        return 0;
    if (!weechat_infolist_new_var_time (ptr_item, "last_check_time", xfer->last_check_time))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->last_check_pos);
    if (!weechat_infolist_new_var_string (ptr_item, "last_check_pos", value))
        return 0;
    if (!weechat_infolist_new_var_time (ptr_item, "last_activity", xfer->last_activity))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->bytes_per_sec);
    if (!weechat_infolist_new_var_string (ptr_item, "bytes_per_sec", value))
        return 0;
    snprintf (value, sizeof (value), "%llu", xfer->eta);
    if (!weechat_infolist_new_var_string (ptr_item, "eta", value))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "hash_target", xfer->hash_target))
        return 0;
    if (!weechat_infolist_new_var_integer (ptr_item, "hash_status", xfer->hash_status))
        return 0;
    if (!weechat_infolist_new_var_string (ptr_item, "hash_status_string", xfer_hash_status_string[xfer->hash_status]))
        return 0;

    return 1;
}

/*
 * Prints xfer infos in WeeChat log file (usually for crash dump).
 */

void
xfer_print_log ()
{
    struct t_xfer *ptr_xfer;

    for (ptr_xfer = xfer_list; ptr_xfer; ptr_xfer = ptr_xfer->next_xfer)
    {
        weechat_log_printf ("");
        weechat_log_printf ("[xfer (addr:0x%lx)]", ptr_xfer);
        weechat_log_printf ("  plugin_name . . . . . . : '%s'",  ptr_xfer->plugin_name);
        weechat_log_printf ("  plugin_id . . . . . . . : '%s'",  ptr_xfer->plugin_id);
        weechat_log_printf ("  type. . . . . . . . . . : %d (%s)",
                            ptr_xfer->type,
                            xfer_type_string[ptr_xfer->type]);
        weechat_log_printf ("  protocol. . . . . . . . : %d (%s)",
                            ptr_xfer->protocol,
                            xfer_protocol_string[ptr_xfer->protocol]);
        weechat_log_printf ("  remote_nick . . . . . . : '%s'",  ptr_xfer->remote_nick);
        weechat_log_printf ("  local_nick. . . . . . . : '%s'",  ptr_xfer->local_nick);
        weechat_log_printf ("  charset_modifier. . . . : '%s'",  ptr_xfer->charset_modifier);
        weechat_log_printf ("  filename. . . . . . . . : '%s'",  ptr_xfer->filename);
        weechat_log_printf ("  size. . . . . . . . . . : %llu",  ptr_xfer->size);
        weechat_log_printf ("  proxy . . . . . . . . . : '%s'",  ptr_xfer->proxy);
        weechat_log_printf ("  local_address . . . . . : 0x%lx", ptr_xfer->local_address);
        weechat_log_printf ("  local_address_length. . : %d",    ptr_xfer->local_address_length);
        weechat_log_printf ("  local_address_str . . . : '%s'" , ptr_xfer->local_address_str);
        weechat_log_printf ("  remote_address. . . . . : 0x%lx", ptr_xfer->remote_address);
        weechat_log_printf ("  remote_address_length . : %d",   ptr_xfer->remote_address_length);
        weechat_log_printf ("  remote_address_str. . . : '%s'",  ptr_xfer->remote_address_str);
        weechat_log_printf ("  port. . . . . . . . . . : %d",    ptr_xfer->port);

        weechat_log_printf ("  status. . . . . . . . . : %d (%s)",
                            ptr_xfer->status,
                            xfer_status_string[ptr_xfer->status]);
        weechat_log_printf ("  buffer. . . . . . . . . : 0x%lx", ptr_xfer->buffer);
        weechat_log_printf ("  remote_nick_color . . . : '%s'",  ptr_xfer->remote_nick_color);
        weechat_log_printf ("  fast_send . . . . . . . : %d",    ptr_xfer->fast_send);
        weechat_log_printf ("  blocksize . . . . . . . : %d",    ptr_xfer->blocksize);
        weechat_log_printf ("  start_time. . . . . . . : %lld",  (long long)ptr_xfer->start_time);
        weechat_log_printf ("  start_transfer. . . . . : %lld",  (long long)ptr_xfer->start_transfer);
        weechat_log_printf ("  sock. . . . . . . . . . : %d",    ptr_xfer->sock);
        weechat_log_printf ("  child_pid . . . . . . . : %d",    ptr_xfer->child_pid);
        weechat_log_printf ("  child_read. . . . . . . : %d",    ptr_xfer->child_read);
        weechat_log_printf ("  child_write . . . . . . : %d",    ptr_xfer->child_write);
        weechat_log_printf ("  hook_fd . . . . . . . . : 0x%lx", ptr_xfer->hook_fd);
        weechat_log_printf ("  hook_timer. . . . . . . : 0x%lx", ptr_xfer->hook_timer);
        weechat_log_printf ("  hook_connect. . . . . . : 0x%lx", ptr_xfer->hook_connect);
        weechat_log_printf ("  unterminated_message. . : '%s'",  ptr_xfer->unterminated_message);
        weechat_log_printf ("  file. . . . . . . . . . : %d",    ptr_xfer->file);
        weechat_log_printf ("  local_filename. . . . . : '%s'",  ptr_xfer->local_filename);
        weechat_log_printf ("  temp_local_filename . . : '%s'",  ptr_xfer->temp_local_filename);
        weechat_log_printf ("  filename_suffix . . . . : %d",    ptr_xfer->filename_suffix);
        weechat_log_printf ("  pos . . . . . . . . . . : %llu",  ptr_xfer->pos);
        weechat_log_printf ("  ack . . . . . . . . . . : %llu",  ptr_xfer->ack);
        weechat_log_printf ("  start_resume. . . . . . : %llu",  ptr_xfer->start_resume);
        weechat_log_printf ("  last_check_time . . . . : %lld",  (long long)ptr_xfer->last_check_time);
        weechat_log_printf ("  last_check_pos. . . . . : %llu",  ptr_xfer->last_check_pos);
        weechat_log_printf ("  last_activity . . . . . : %lld",  (long long)ptr_xfer->last_activity);
        weechat_log_printf ("  bytes_per_sec . . . . . : %llu",  ptr_xfer->bytes_per_sec);
        weechat_log_printf ("  eta . . . . . . . . . . : %llu",  ptr_xfer->eta);
        weechat_log_printf ("  hash_target . . . . . . : '%s'",  ptr_xfer->hash_target);
        weechat_log_printf ("  hash_status . . . . . . : %d (%s)",
                            ptr_xfer->hash_status,
                            xfer_hash_status_string[ptr_xfer->hash_status]);
        weechat_log_printf ("  prev_xfer . . . . . . . : 0x%lx", ptr_xfer->prev_xfer);
        weechat_log_printf ("  next_xfer . . . . . . . : 0x%lx", ptr_xfer->next_xfer);
    }
}

/*
 * Callback for signal "debug_dump".
 */

int
xfer_debug_dump_cb (const void *pointer, void *data,
                    const char *signal, const char *type_data,
                    void *signal_data)
{
    /* make C compiler happy */
    (void) pointer;
    (void) data;
    (void) signal;
    (void) type_data;

    if (!signal_data
        || (weechat_strcasecmp ((char *)signal_data, XFER_PLUGIN_NAME) == 0))
    {
        weechat_log_printf ("");
        weechat_log_printf ("***** \"%s\" plugin dump *****",
                            weechat_plugin->name);

        xfer_print_log ();

        weechat_log_printf ("");
        weechat_log_printf ("***** End of \"%s\" plugin dump *****",
                            weechat_plugin->name);
    }

    return WEECHAT_RC_OK;
}

/*
 * Initializes xfer plugin.
 */

int
weechat_plugin_init (struct t_weechat_plugin *plugin, int argc, char *argv[])
{
    /* make C compiler happy */
    (void) argc;
    (void) argv;

    weechat_plugin = plugin;

    if (!xfer_config_init ())
        return WEECHAT_RC_ERROR;

    xfer_config_read ();

    xfer_create_directories ();

    xfer_command_init ();

    /* hook some signals */
    weechat_hook_signal ("upgrade",
                         &xfer_signal_upgrade_cb, NULL, NULL);
    weechat_hook_signal ("xfer_add",
                         &xfer_add_cb, NULL, NULL);
    weechat_hook_signal ("xfer_start_resume",
                         &xfer_start_resume_cb, NULL, NULL);
    weechat_hook_signal ("xfer_accept_resume",
                         &xfer_accept_resume_cb, NULL, NULL);
    weechat_hook_signal ("debug_dump",
                         &xfer_debug_dump_cb, NULL, NULL);

    /* hook completions */
    xfer_completion_init ();

    xfer_info_init ();

    if (weechat_xfer_plugin->upgrading)
        xfer_upgrade_load ();

    return WEECHAT_RC_OK;
}

/*
 * Ends xfer plugin.
 */

int
weechat_plugin_end (struct t_weechat_plugin *plugin)
{
    /* make C compiler happy */
    (void) plugin;

    xfer_config_write ();

    if (xfer_signal_upgrade_received)
        xfer_upgrade_save ();
    else
        xfer_disconnect_all ();

    xfer_free_all ();

    return WEECHAT_RC_OK;
}
