/*
 *  sock.c
 *
 *  Copyright (C) 1995, 1996 by Paal-Kr. Engstad and Volker Lendecke
 *  Copyright (C) 1997 by Volker Lendecke
 *
 *  Please add a note about your changes to smbfs in the ChangeLog file.
 */

#include <linux/fs.h>
#include <linux/time.h>
#include <linux/errno.h>
#include <linux/socket.h>
#include <linux/fcntl.h>
#include <linux/file.h>
#include <linux/in.h>
#include <linux/net.h>
#include <linux/tcp.h>
#include <linux/mm.h>
#include <linux/netdevice.h>
#include <linux/smp_lock.h>
#include <linux/workqueue.h>
#include <linux/net.h>
#include <net/scm.h>
#include <net/ip.h>

#include <linux/smb_fs.h>
#include <linux/smb.h>
#include <linux/smbno.h>

#include <asm/uaccess.h>
#include <asm/ioctls.h>

#include "smb_debug.h"
#include "proto.h"
#include "request.h"


static int
_recvfrom(struct socket *socket, unsigned char *ubuf, int size, unsigned flags)
{
	struct iovec iov;
	struct msghdr msg;
	mm_segment_t fs;

	fs = get_fs();
	set_fs(get_ds());
	flags |= MSG_DONTWAIT | MSG_NOSIGNAL;

	msg.msg_flags = flags;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = NULL;
	iov.iov_base = ubuf;
	iov.iov_len = size;

	size = sock_recvmsg(socket, &msg, size, flags);

	set_fs(fs);
	return size;
}

/*
 * Return the server this socket belongs to
 */
static struct smb_sb_info *
server_from_socket(struct socket *socket)
{
	return socket->sk->user_data;
}

/*
 * Called when there is data on the socket.
 */
void
smb_data_ready(struct sock *sk, int len)
{
	struct smb_sb_info *server = server_from_socket(sk->socket);
	void (*data_ready)(struct sock *, int) = server->data_ready;

	data_ready(sk, len);
	VERBOSE("(%p, %d)\n", sk, len);
	smbiod_wake_up();
}

int
smb_valid_socket(struct inode * inode)
{
	return (inode && S_ISSOCK(inode->i_mode) && 
		SOCKET_I(inode)->type == SOCK_STREAM);
}

static struct socket *
server_sock(struct smb_sb_info *server)
{
	struct file *file;

	if (server && (file = server->sock_file))
	{
#ifdef SMBFS_PARANOIA
		if (!smb_valid_socket(file->f_dentry->d_inode))
			PARANOIA("bad socket!\n");
#endif
		return SOCKET_I(file->f_dentry->d_inode);
	}
	return NULL;
}

void
smb_close_socket(struct smb_sb_info *server)
{
	struct file * file = server->sock_file;

	if (file) {
		struct socket *sock = server_sock(server);

		VERBOSE("closing socket %p\n", sock);
		sock->sk->data_ready = server->data_ready;
		server->sock_file = NULL;
		fput(file);
	}
}

static int
smb_get_length(struct socket *socket, unsigned char *header)
{
	int result;

	result = _recvfrom(socket, header, 4, MSG_PEEK);
	if (result == -EAGAIN)
		return -ENODATA;
	if (result < 0) {
		PARANOIA("recv error = %d\n", -result);
		return result;
	}
	if (result < 4)
		return -ENODATA;

	switch (header[0]) {
	case 0x00:
	case 0x82:
		break;

	case 0x85:
		DEBUG1("Got SESSION KEEP ALIVE\n");
		_recvfrom(socket, header, 4, 0);	/* read away */
		return -ENODATA;

	default:
		PARANOIA("Invalid NBT packet, code=%x\n", header[0]);
		return -EIO;
	}

	/* The length in the RFC NB header is the raw data length */
	return smb_len(header);
}

int
smb_recv_available(struct smb_sb_info *server)
{
	mm_segment_t oldfs;
	int avail, err;
	struct socket *sock = server_sock(server);

	oldfs = get_fs();
	set_fs(get_ds());
	err = sock->ops->ioctl(sock, SIOCINQ, (unsigned long) &avail);
	set_fs(oldfs);
	return (err >= 0) ? avail : err;
}

/*
 * Adjust the iovec to move on 'n' bytes (from nfs/sunrpc)
 */
static int
smb_move_iov(struct msghdr *msg, struct iovec *niv, unsigned amount)
{
	struct iovec *iv = msg->msg_iov;
	int i;
	int len;

	/*
	 *	Eat any sent iovecs
	 */
	while (iv->iov_len <= amount) {
		amount -= iv->iov_len;
		iv++;
		msg->msg_iovlen--;
	}

	/*
	 *	And chew down the partial one
	 */
	niv[0].iov_len = iv->iov_len-amount;
	niv[0].iov_base =((unsigned char *)iv->iov_base)+amount;
	iv++;

	len = niv[0].iov_len;

	/*
	 *	And copy any others
	 */
	for (i = 1; i < msg->msg_iovlen; i++) {
		niv[i] = *iv++;
		len += niv[i].iov_len;
	}

	msg->msg_iov = niv;
	return len;
}

/*
 * smb_receive_header
 * Only called by the smbiod thread.
 */
int
smb_receive_header(struct smb_sb_info *server)
{
	struct socket *sock;
	int result = 0;
	unsigned char peek_buf[4];

	result = -EIO; 
	sock = server_sock(server);
	if (!sock)
		goto out;
	if (sock->sk->state != TCP_ESTABLISHED)
		goto out;

	if (!server->smb_read) {
		result = smb_get_length(sock, peek_buf);
		if (result < 0) {
			if (result == -ENODATA)
				result = 0;
			goto out;
		}
		server->smb_len = result + 4;

		if (server->smb_len < SMB_HEADER_LEN) {
			PARANOIA("short packet: %d\n", result);
			server->rstate = SMB_RECV_DROP;
			result = -EIO;
			goto out;
		}
		if (server->smb_len > SMB_MAX_PACKET_SIZE) {
			PARANOIA("long packet: %d\n", result);
			server->rstate = SMB_RECV_DROP;
			result = -EIO;
			goto out;
		}
	}

	result = _recvfrom(sock, server->header + server->smb_read,
			   SMB_HEADER_LEN - server->smb_read, 0);
	VERBOSE("_recvfrom: %d\n", result);
	if (result < 0) {
		VERBOSE("receive error: %d\n", result);
		goto out;
	}
	server->smb_read += result;

	if (server->smb_read == SMB_HEADER_LEN)
		server->rstate = SMB_RECV_HCOMPLETE;
out:
	return result;
}

static char drop_buffer[PAGE_SIZE];

/*
 * smb_receive_drop - read and throw away the data
 * Only called by the smbiod thread.
 *
 * FIXME: we are in the kernel, could we just tell the socket that we want
 * to drop stuff from the buffer?
 */
int
smb_receive_drop(struct smb_sb_info *server)
{
	struct socket *sock;
	unsigned int flags;
	struct iovec iov;
	struct msghdr msg;
	mm_segment_t fs;
	int rlen = smb_len(server->header) - server->smb_read + 4;
	int result = -EIO;

	sock = server_sock(server);
	if (!sock)
		goto out;
	if (sock->sk->state != TCP_ESTABLISHED)
		goto out;

	fs = get_fs();
	set_fs(get_ds());

	flags = MSG_DONTWAIT | MSG_NOSIGNAL;
	iov.iov_base = drop_buffer;
	iov.iov_len = PAGE_SIZE;
	msg.msg_flags = flags;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = NULL;

	if (rlen > PAGE_SIZE)
		rlen = PAGE_SIZE;

	result = sock_recvmsg(sock, &msg, rlen, flags);

	set_fs(fs);

	VERBOSE("read: %d\n", result);
	if (result < 0) {
		VERBOSE("receive error: %d\n", result);
		goto out;
	}
	server->smb_read += result;

	if (server->smb_read >= server->smb_len)
		server->rstate = SMB_RECV_END;

out:
	return result;
}

/*
 * smb_receive
 * Only called by the smbiod thread.
 */
int
smb_receive(struct smb_sb_info *server, struct smb_request *req)
{
	struct socket *sock;
	unsigned int flags;
	struct iovec iov[4];
	struct msghdr msg;
	mm_segment_t fs;
	int rlen;
	int result = -EIO;

	sock = server_sock(server);
	if (!sock)
		goto out;
	if (sock->sk->state != TCP_ESTABLISHED)
		goto out;

	fs = get_fs();
	set_fs(get_ds());

	flags = MSG_DONTWAIT | MSG_NOSIGNAL;;
	msg.msg_flags = flags;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = req->rq_iov;
	msg.msg_iovlen = req->rq_iovlen;
	msg.msg_control = NULL;

	/* Dont repeat bytes and count available bufferspace */
	rlen = smb_move_iov(&msg, iov, req->rq_bytes_recvd);
	if (req->rq_rlen < rlen)
		rlen = req->rq_rlen;

	result = sock_recvmsg(sock, &msg, rlen, flags);

	set_fs(fs);

	VERBOSE("read: %d\n", result);
	if (result < 0) {
		VERBOSE("receive error: %d\n", result);
		goto out;
	}
	req->rq_bytes_recvd += result;
	server->smb_read += result;

out:
	return result;
}

/*
 * Try to send a SMB request. This may return after sending only parts of the
 * request. SMB_REQ_TRANSMITTED will be set if a request was fully sent.
 *
 * Parts of this was taken from xprt_sendmsg from net/sunrpc/xprt.c
 */
int
smb_send_request(struct smb_request *req)
{
	mm_segment_t fs;
	struct smb_sb_info *server = req->rq_server;
	struct socket *sock;
	struct msghdr msg;
        int slen = req->rq_slen - req->rq_bytes_sent;
	int result = -EIO;
	struct iovec iov[4];

	sock = server_sock(server);
	if (!sock)
		goto out;
	if (sock->sk->state != TCP_ESTABLISHED)
		goto out;

	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_iov = req->rq_iov;
	msg.msg_iovlen = req->rq_iovlen;
	msg.msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT;

	/* Dont repeat bytes */
	if (req->rq_bytes_sent)
		smb_move_iov(&msg, iov, req->rq_bytes_sent);

	fs = get_fs();
	set_fs(get_ds());
	result = sock_sendmsg(sock, &msg, slen);
	set_fs(fs);

	if (result >= 0) {
		req->rq_bytes_sent += result;
		if (req->rq_bytes_sent >= req->rq_slen)
			req->rq_flags |= SMB_REQ_TRANSMITTED;
	}
out:
	return result;
}
