/*
 * Copyright (c) 2003-2006  Motoyuki Kasahara
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "build-pre.h"

#include <sys/socket.h>
#include <sys/time.h>

#include "eb.h"
#include "error.h"
#include "build-post.h"
#include "dummyin6.h"

#if !defined(HAVE_GETADDRINFO) || !defined(HAVE_GETNAMEINFO)
#include "getaddrinfo.h"
#endif

#include "ebnet.h"
#include "linebuf.h"
#include "urlparts.h"

#ifndef IF_NAMESIZE
#ifdef IFNAMSIZ
#define IF_NAMESIZE             IFNAMSIZ
#else
#define IF_NAMESIZE             16
#endif
#endif

#ifndef NI_MAXHOST
#define NI_MAXHOST		1025
#endif

#ifndef SHUT_RD
#define SHUT_RD 0
#endif
#ifndef SHUT_WR
#define SHUT_WR 1
#endif
#ifndef SHUT_RDWR
#define SHUT_RDWR 2
#endif

/*
 * Max retry count for establishing a new connection with the server.
 */
#define EBNET_MAX_RETRY_COUNT	1

/*
 * Unexported functions.
 */
static int ebnet_parse_booklist_entry(const char *line, char *book_name,
    char *book_title);
static int ebnet_send_quit(int file);
static int ebnet_parse_url(const char *url, char *host, in_port_t *port,
    char *book_name, char *file_path);
static int is_integer(const char *string);
static int write_string_all(int file, int timeout, const char *string);


/*
 * Initialize ebnet.
 */
void
ebnet_initialize(void)
{
#ifdef WINSOCK
    WSADATA wsa_data;

    WSAStartup(MAKEWORD(2, 2), &wsa_data);
#endif
    ebnet_initialize_multiplex();
    ebnet_set_bye_hook(ebnet_send_quit);
}


/*
 * Get a book list from a server.
 */
EB_Error_Code
ebnet_bind_booklist(EB_BookList *booklist, const char *url)
{
    EB_Error_Code error_code;
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char book_title[EBNET_MAX_BOOK_TITLE_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    int ebnet_file = -1;
    ssize_t read_result;
    int lost_sync;
    int retry_count = 0;

    LOG(("in: ebnet_bind_booklist(url=%s)", url));

  retry:
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name != '\0') {
	error_code = EB_ERR_BAD_FILE_NAME;
	goto failed;
    }

    /*
     * Establish a connection.
     */
    ebnet_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (ebnet_file < 0) {
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }

    /*
     * Request BOOKLIST.
     */
    bind_file_to_line_buffer(&line_buffer, ebnet_file);
    sprintf(line, "BOOKLIST %s\r\n", book_name);
    if (write_string_all(ebnet_file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0) {
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }

    /*
     * Get a list.
     */
    for (;;) {
	read_result = read_line_buffer(&line_buffer, line, sizeof(line));
	if (read_result < 0 || read_result == sizeof(line)) {
	    lost_sync = 1;
	    error_code = EB_ERR_EBNET_FAIL_CONNECT;
	    goto failed;
	}
	if (*line == '\0')
	    break;
	if (ebnet_parse_booklist_entry(line, book_name, book_title) < 0) {
	    lost_sync = 1;
	    error_code = EB_ERR_EBNET_FAIL_CONNECT;
	    goto failed;
	}
	error_code = eb_booklist_add_book(booklist, book_name, book_title);
	if (error_code != EB_SUCCESS)
	    goto failed;
    }

    ebnet_disconnect_socket(ebnet_file);
    finalize_line_buffer(&line_buffer);
    LOG(("out: ebnet_bind_booklist() = %s", eb_error_string(EB_SUCCESS)));
    return EB_SUCCESS;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= ebnet_file) {
	if (lost_sync) {
	    shutdown(ebnet_file, SHUT_RDWR);
	    ebnet_set_lost_sync(ebnet_file);
	}
	ebnet_disconnect_socket(ebnet_file);
	ebnet_file = -1;
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    LOG(("out: ebnet_bind_booklist() = %s", eb_error_string(error_code)));
    return error_code;
}


static int
ebnet_parse_booklist_entry(const char *line, char *book_name, char *book_title)
{
    const char *space;
    size_t book_name_length;
    size_t book_title_length;
    char *p;

    space = strchr(line, ' ');
    if (space == NULL)
	return -1;
    book_name_length = space - line;
    book_title_length = strlen(space + 1);

    if (book_name_length == 0
	|| EBNET_MAX_BOOK_NAME_LENGTH < book_name_length)
	return -1;
    if (book_title_length == 0
	|| EBNET_MAX_BOOK_TITLE_LENGTH < book_title_length)
	return -1;

    memcpy(book_name, line, book_name_length);
    *(book_name + book_name_length) = '\0';
    memcpy(book_title, space + 1, book_title_length);
    *(book_title + book_title_length) = '\0';

    for (p = book_name; *p != '\0'; p++) {
	if (!ASCII_ISLOWER(*p) && !ASCII_ISDIGIT(*p)
	    && *p != '_' && *p != '-' && *p != '.')
	    return -1;
    }

    return 0;
}


/*
 * Extension code for eb_bind() to support ebnet.
 */
EB_Error_Code
ebnet_bind(EB_Book *book, const char *url)
{
    EB_Error_Code error_code;
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    ssize_t read_result;
    int lost_sync;
    int retry_count = 0;

    LOG(("in: ebnet_bind(url=%s)", url));

  retry:
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name == '\0') {
	error_code = EB_ERR_BAD_FILE_NAME;
	goto failed;
    }

    /*
     * Establish a connection.
     */
    book->ebnet_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (book->ebnet_file < 0) {
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }

    ebnet_set_book_name(book->ebnet_file, book_name);

    /*
     * Request BOOK.
     */
    bind_file_to_line_buffer(&line_buffer, book->ebnet_file);
    sprintf(line, "BOOK %s\r\n", book_name);
    if (write_string_all(book->ebnet_file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	error_code = EB_ERR_FAIL_OPEN_CAT;
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0) {
	if (strncasecmp(line, "!BUSY;", 6) == 0)
	    error_code = EB_ERR_EBNET_SERVER_BUSY;
	else if (strncasecmp(line, "!PERM;", 6) == 0)
	    error_code = EB_ERR_EBNET_NO_PERMISSION;
	else
	    error_code = EB_ERR_FAIL_OPEN_CAT;
	goto failed;
    }

    finalize_line_buffer(&line_buffer);
    LOG(("out: ebnet_bind() = %s", eb_error_string(EB_SUCCESS)));
    return EB_SUCCESS;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= book->ebnet_file) {
	if (lost_sync) {
	    shutdown(book->ebnet_file, SHUT_RDWR);
	    ebnet_set_lost_sync(book->ebnet_file);
	}
	ebnet_disconnect_socket(book->ebnet_file);
	book->ebnet_file = -1;
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    LOG(("out: ebnet_bind() = %s", eb_error_string(error_code)));
    return error_code;
}


/*
 * Extension code for eb_bind_appendix() to support ebnet.
 */
EB_Error_Code
ebnet_bind_appendix(EB_Appendix *appendix, const char *url)
{
    EB_Error_Code error_code;
    char host[NI_MAXHOST];
    in_port_t port;
    char appendix_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    ssize_t read_result;
    int lost_sync;
    int retry_count = 0;

    LOG(("in: ebnet_bind(url=%s)", url));

  retry:
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, appendix_name, url_path) < 0
	|| *appendix_name == '\0') {
	error_code = EB_ERR_BAD_FILE_NAME;
	goto failed;
    }

    /*
     * Establish a connection.
     */
    appendix->ebnet_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (appendix->ebnet_file < 0) {
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }

    ebnet_set_book_name(appendix->ebnet_file, appendix_name);

    /*
     * Request BOOK.
     */
    bind_file_to_line_buffer(&line_buffer, appendix->ebnet_file);
    sprintf(line, "BOOK %s\r\n", appendix_name);
    if (write_string_all(appendix->ebnet_file, EBNET_TIMEOUT_SECONDS, line)
	<= 0) {
	error_code = EB_ERR_FAIL_OPEN_CAT;
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	error_code = EB_ERR_EBNET_FAIL_CONNECT;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0) {
	if (strncasecmp(line, "!BUSY;", 6) == 0)
	    error_code = EB_ERR_EBNET_SERVER_BUSY;
	else if (strncasecmp(line, "!PERM;", 6) == 0)
	    error_code = EB_ERR_EBNET_NO_PERMISSION;
	else
	    error_code = EB_ERR_FAIL_OPEN_CAT;
	goto failed;
    }

    finalize_line_buffer(&line_buffer);
    LOG(("out: ebnet_bind() = %s", eb_error_string(EB_SUCCESS)));
    return EB_SUCCESS;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= appendix->ebnet_file) {
	if (lost_sync) {
	    shutdown(appendix->ebnet_file, SHUT_RDWR);
	    ebnet_set_lost_sync(appendix->ebnet_file);
	}
	ebnet_disconnect_socket(appendix->ebnet_file);
	appendix->ebnet_file = -1;
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    LOG(("out: ebnet_bind() = %s", eb_error_string(error_code)));
    return error_code;
}


/*
 * Extension code for eb_finalize_book() to support ebnet.
 */
void
ebnet_finalize_book(EB_Book *book)
{
    LOG(("in+out: ebnet_finalize_book(book=%d)", (int)book->code));

    if (0 <= book->ebnet_file) {
	ebnet_disconnect_socket(book->ebnet_file);
	book->ebnet_file = -1;
    }
}


/*
 * Extension code for eb_finalize_appendix() to support ebnet.
 */
void
ebnet_finalize_appendix(EB_Appendix *appendix)
{
    LOG(("in+out: ebnet_finalize_appendix(appendix=%d)", (int)appendix->code));

    if (0 <= appendix->ebnet_file) {
	ebnet_disconnect_socket(appendix->ebnet_file);
	appendix->ebnet_file = -1;
    }
}


/*
 * Extension code for zio_open_raw() to support ebnet.
 */
int
ebnet_open(const char *url)
{
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    ssize_t read_result;
    ssize_t file_size;
    int new_file;
    int lost_sync;
    int retry_count = 0;

    LOG(("in: ebnet_open(url=%s)", url));

  retry:
    new_file = -1;
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name == '\0') {
	goto failed;
    }

    /*
     * Connect with a server.
     */
    new_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (new_file < 0)
	goto failed;

    ebnet_set_book_name(new_file, book_name);
    ebnet_set_file_path(new_file, url_path);

    /*
     * Request FILESIZE.
     */
    bind_file_to_line_buffer(&line_buffer, new_file);
    sprintf(line, "FILESIZE %s /%s\r\n", book_name, url_path);
    if (write_string_all(new_file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0)
	goto failed;

    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || !is_integer(line)) {
	lost_sync = 1;
	goto failed;
    }

    file_size = atoi(line);
    if (file_size < 0)
	goto failed;
    ebnet_set_file_size(new_file, file_size);

    finalize_line_buffer(&line_buffer);
    LOG(("out: ebnet_open() = %d", new_file));
    return new_file;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= new_file) {
	if (lost_sync) {
	    shutdown(new_file, SHUT_RDWR);
	    ebnet_set_lost_sync(new_file);
	}
	ebnet_disconnect_socket(new_file);
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    LOG(("out: ebnet_open() = %d", -1));
    return -1;
}


/*
 * Extension code for zio_close_raw() to support ebnet.
 */
int
ebnet_close(int file)
{
    LOG(("in+out: ebnet_close(file=%d)", file));
    ebnet_disconnect_socket(file);
    return 0;
}


/*
 * Extension code for zio_lseek_raw() to support ebnet.
 */
off_t
ebnet_lseek(int file, off_t offset, int whence)
{
    ssize_t file_size;
    off_t new_offset = 0;

    LOG(("in: ebnet_lseek(file=%d, location=%ld, whence=%d)", file,
	(long)offset, whence));

    file_size = ebnet_get_file_size(file);
    if (file_size < 0)
	goto failed;

    switch (whence) {
    case SEEK_SET:
	new_offset = offset;
	break;
    case SEEK_CUR:
	new_offset = new_offset + offset;
	break;
    case SEEK_END:
	if (offset <= file_size)
	    new_offset = file_size - offset;
	else
	    new_offset = 0;
	break;
    default:
	goto failed;
    }

    ebnet_set_offset(file, new_offset);
    LOG(("out: ebnet_lseek() = %ld", (long)new_offset));
    return new_offset;

    /*
     * An error occurs...
     */
  failed:
    LOG(("out: ebnet_lseek() = %ld", (long)-1));
    return -1;
}


/*
 * Extension code for zio_read_raw() to support ebnet.
 */
ssize_t
ebnet_read(int *file, char *buffer, size_t length)
{
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    const char *book_name;
    const char *url_path;
    off_t offset;
    size_t received_length;
    ssize_t read_result;
    ssize_t chunk_length;
    int lost_sync;
    int retry_count = 0;

    LOG(("in: ebnet_read(*file=%d, length=%ld)", *file, (long)length));

    if (length == 0) {
	LOG(("out: ebnet_read() = %ld", (long)0));
	return 0;
    }

  retry:
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);

    /*
     * Request READ.
     */
    book_name = ebnet_get_book_name(*file);
    url_path = ebnet_get_file_path(*file);
    offset = ebnet_get_offset(*file);
    if (book_name == NULL || url_path == NULL || offset < 0)
	goto failed;

    bind_file_to_line_buffer(&line_buffer, *file);
    sprintf(line, "READ %s /%s %ld %ld\r\n", book_name, url_path,
	(long)offset, (long)length);
    if (write_string_all(*file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0)
	goto failed;

    received_length = 0;
    while (received_length < length) {
	read_result = read_line_buffer(&line_buffer, line, sizeof(line));
	if (read_result < 0 || read_result == sizeof(line) || *line != '*') {
	    lost_sync = 1;
	    goto failed;
	}

	if (!is_integer(line + 1)) {
	    lost_sync = 1;
	    goto failed;
	} else if (strcmp(line + 1, "-1") == 0) {
	    ebnet_set_offset(*file, offset + received_length);
	    goto failed;
	} else if (strcmp(line + 1, "0") == 0) {
	    break;
	}
	chunk_length = atoi(line + 1);
	if (chunk_length <= 0 || length < received_length + chunk_length) {
	    lost_sync = 1;
	    goto failed;
	}

	read_result = binary_read_line_buffer(&line_buffer,
	    buffer + received_length, chunk_length);
	if (read_result != chunk_length) {
	    lost_sync = 1;
	    goto failed;
	}
	received_length += chunk_length;
    }

    ebnet_set_offset(*file, offset + received_length);
    finalize_line_buffer(&line_buffer);
    LOG(("out: ebnet_read(*file=%d) = %ld", *file, (long)received_length));
    return received_length;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (lost_sync) {
	shutdown(*file, SHUT_RDWR);
	ebnet_set_lost_sync(*file);
	if (retry_count < EBNET_MAX_RETRY_COUNT) {
	    int new_file = ebnet_reconnect_socket(*file);
	    if (0 <= new_file) {
		*file = new_file;
		retry_count++;
		goto retry;
	    }
	}
    }
    LOG(("out: ebnet_read(*file=%d) = %ld", *file, (long)-1));
    return -1;
}


/*
 * Extension code for eb_fix_directory_name() to support ebnet.
 */
EB_Error_Code
ebnet_fix_directory_name(const char *url, char *directory_name)
{
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    ssize_t read_result;
    int new_file;
    int lost_sync;
    int retry_count = 0;

  retry:
    new_file = -1;
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name == '\0') {
	goto failed;
    }

    /*
     * Connect with a server.
     */
    new_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (new_file < 0)
	goto failed;

    /*
     * Request DIR.
     */
    bind_file_to_line_buffer(&line_buffer, new_file);
    sprintf(line, "DIR %s /%s %s\r\n", book_name, url_path, directory_name);
    if (write_string_all(new_file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0)
	goto failed;

    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line)) {
	lost_sync = 1;
	goto failed;
    }

    if (*line == '\0' || EB_MAX_DIRECTORY_NAME_LENGTH < strlen(line))
	goto failed;
    strcpy(directory_name, line);

    finalize_line_buffer(&line_buffer);
    ebnet_disconnect_socket(new_file);
    return EB_SUCCESS;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= new_file) {
	if (lost_sync) {
	    shutdown(new_file, SHUT_RDWR);
	    ebnet_set_lost_sync(new_file);
	}
	ebnet_disconnect_socket(new_file);
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    return EB_ERR_BAD_DIR_NAME;
}


/*
 * Extension code for eb_find_file_name() to support ebnet.
 */
EB_Error_Code
ebnet_find_file_name(const char *url, const char *target_file_name,
    char *found_file_name)
{
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];
    Line_Buffer line_buffer;
    char line[EBNET_MAX_LINE_LENGTH + 1];
    ssize_t read_result;
    int new_file;
    int lost_sync;
    int retry_count = 0;

  retry:
    new_file = -1;
    lost_sync = 0;
    initialize_line_buffer(&line_buffer);
    set_line_buffer_timeout(&line_buffer, EBNET_TIMEOUT_SECONDS);

    /*
     * Parse URL.
     */
    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name == '\0') {
	goto failed;
    }

    /*
     * Connect with a server.
     */
    new_file = ebnet_connect_socket(host, port, PF_UNSPEC);
    if (new_file < 0)
	goto failed;

    /*
     * Request FILE.
     */
    bind_file_to_line_buffer(&line_buffer, new_file);
    sprintf(line, "FILE %s /%s %s\r\n", book_name, url_path, target_file_name);
    if (write_string_all(new_file, EBNET_TIMEOUT_SECONDS, line) <= 0) {
	lost_sync = 1;
	goto failed;
    }
    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line) || *line != '!') {
	lost_sync = 1;
	goto failed;
    }
    if (strncasecmp(line, "!OK;", 4) != 0)
	goto failed;

    read_result = read_line_buffer(&line_buffer, line, sizeof(line));
    if (read_result < 0 || read_result == sizeof(line)) {
	lost_sync = 1;
	goto failed;
    }

    if (*line == '\0' || EB_MAX_FILE_NAME_LENGTH < strlen(line))
	goto failed;
    strcpy(found_file_name, line);

    finalize_line_buffer(&line_buffer);
    ebnet_disconnect_socket(new_file);
    return EB_SUCCESS;

    /*
     * An error occurs...
     */
  failed:
    finalize_line_buffer(&line_buffer);
    if (0 <= new_file) {
	if (lost_sync) {
	    shutdown(new_file, SHUT_RDWR);
	    ebnet_set_lost_sync(new_file);
	}
	ebnet_disconnect_socket(new_file);
	if (lost_sync && retry_count < EBNET_MAX_RETRY_COUNT) {
	    retry_count++;
	    goto retry;
	}
    }
    return EB_ERR_BAD_FILE_NAME;
}


/*
 * Bye hook.
 * We must send `QUIT' command before close a connection.
 */
static int
ebnet_send_quit(int file)
{
    if (write_string_all(file, EBNET_TIMEOUT_SECONDS, "QUIT\r\n") <= 0)
	return -1;
    return 0;
}


/*
 * URL version of eb_canonicalize_path_name().
 */
EB_Error_Code
ebnet_canonicalize_url(char *url)
{
    char host[NI_MAXHOST];
    in_port_t port;
    char book_name[EBNET_MAX_BOOK_NAME_LENGTH + 1];
    char url_path[EB_MAX_RELATIVE_PATH_LENGTH + 1];

    if (ebnet_parse_url(url, host, &port, book_name, url_path) < 0
	|| *book_name == '\0') {
	return EB_ERR_BAD_FILE_NAME;
    }

    /*
     * "ebnet://[<host>]:<port>/<book_name>" must not exceed
     * EB_MAX_PATH_LENGTH.
     *
     * Note:
     *   "ebnet://[" + "]:" + "/" = 12 characters.
     *    <port> is 5 characters maximum.
     */
    if (EB_MAX_PATH_LENGTH
	< strlen(host) + strlen(book_name) + strlen(url_path) + 17)
	return EB_ERR_TOO_LONG_FILE_NAME;

    if (strchr(host, ':') != NULL)
	sprintf(url, "ebnet://[%s]:%d/%s", host, (int)port, book_name);
    else
	sprintf(url, "ebnet://%s:%d/%s", host, (int)port, book_name);

    return EB_SUCCESS;
}


/*
 * Parse URL.
 *
 * As the result, host, port, book_name and file-path are put into
 * the corresponding arguments.
 */
static int
ebnet_parse_url(const char *url, char *host, in_port_t *port,
    char *book_name, char *file_path)
{
    URL_Parts parts;
    const char *scheme_part;
    const char *host_part;
    const char *port_part;
    const char *port_part_p;
    const char *path_part;
    const char *slash;
    size_t book_name_length;

    *host = '\0';
    *port = 0;
    *book_name = '\0';
    *file_path = '\0';

    url_parts_initialize(&parts);
    if (url_parts_parse(&parts, url) < 0)
	goto failed;

    /*
     * Check scheme.
     */
    scheme_part = url_parts_scheme(&parts);
    if (scheme_part == NULL || strcmp(scheme_part, EBNET_SERVICE_NAME) != 0)
	goto failed;

    /*
     * Check host.
     */
    host_part = url_parts_host(&parts);
    if (host_part == NULL || *host_part == '\0'
	|| NI_MAXHOST < strlen(host_part) + 1)
	goto failed;
    strcpy(host, host_part);

    /*
     * Check port.
     */
    port_part = url_parts_port(&parts);
    if (port_part == NULL || *port_part == '\0')
	port_part = EBNET_DEFAULT_PORT;

    for (port_part_p = port_part; *port_part_p != '\0'; port_part_p++) {
	if (!ASCII_ISDIGIT(*port_part_p))
	    goto failed;
    }
    *port = atoi(port_part);

    /*
     * Check path.
     */
    path_part = url_parts_path(&parts);
    if (path_part == NULL || *path_part == '\0') {
	*book_name = '\0';
	*file_path = '\0';
    } else {
	slash = strchr(path_part + 1, '/');
	if (slash == NULL) {
	    book_name_length = strlen(path_part + 1);
	    if (EBNET_MAX_BOOK_NAME_LENGTH < book_name_length)
		goto failed;
	    strcpy(book_name, path_part + 1);
	    *file_path = '\0';
	} else {
	    book_name_length = slash - (path_part + 1);
	    if (book_name_length == 0
		|| EBNET_MAX_BOOK_NAME_LENGTH < book_name_length)
		goto failed;
	    memcpy(book_name, path_part + 1, book_name_length);
	    *(book_name + book_name_length) = '\0';

	    if (EB_MAX_RELATIVE_PATH_LENGTH < strlen(slash + 1))
		goto failed;
	    strcpy(file_path, slash + 1);
	}
    }

    url_parts_finalize(&parts);
    return 0;

    /*
     * An error occurs...
     */
  failed:
    *host = '\0';
    *port = 0;
    *book_name = '\0';
    *file_path = '\0';
    url_parts_finalize(&parts);
    return -1;
}


/*
 * Check whether `string' is integer or not.
 * Return 1 if it is, 0 otherwise.
 */
static int
is_integer(const char *string)
{
    const char *s = string;

    if (*s == '-')
	s++;
    if (!ASCII_ISDIGIT(*s))
	return 0;
    s++;

    while (*s != '\0') {
	if (!ASCII_ISDIGIT(*s))
	    return 0;
	s++;
    }

    return 1;
}


/*
 * Write data to a file.
 * It repeats to call write() until all data will have written.
 * The function returns 1 upon success, 0 upon timeout, -1 upon error.
 */
static int
write_string_all(int file, int timeout, const char *string)
{
    const char *string_p = string;
    ssize_t rest_length = strlen(string);
    fd_set fdset;
    struct timeval timeval;
    int select_result;
    ssize_t write_result;

    while (0 < rest_length) {
	errno = 0;
	FD_ZERO(&fdset);
	FD_SET(file, &fdset);

	if (timeout == 0)
	    select_result = select(file + 1, NULL, &fdset, NULL, NULL);
	else {
	    timeval.tv_sec = timeout;
	    timeval.tv_usec = 0;
	    select_result = select(file + 1, NULL, &fdset, NULL, &timeval);
	}
	if (select_result < 0) {
	    if (errno == EINTR)
		continue;
	    return -1;
	} else if (select_result == 0) {
	    return 0;
	}

	errno = 0;
	write_result = send(file, string_p, rest_length, 0);
	if (write_result < 0) {
	    if (errno == EINTR)
		continue;
	    return -1;
	} else {
	    rest_length -= write_result;
	    string_p += write_result;
	}
    }

    return 1;
}


