/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
/*
 * SMB Locking library functions.
 *
 * You will notice that the functions in this file exit the lock of the session
 * and reenter it before returning. They even assume that the lock has been
 * entered in READER mode. The reason for that is a potential deadlock that may
 * occur when an oplock needs to be broken and the function
 * smb_session_break_oplock() is called. It should be noticed that the mutex of
 * the smb node, the oplock of which needs to be broken, is also exited before
 * calling smb_session_break_oplock(). The reason for that is the same: avoiding
 * a deadlock. That complexity is due to the fact that the lock of the session
 * is held during the treatment of a request. That complexity will go away when
 * that is not the case anymore.
 */

#include <smbsrv/smb_kproto.h>
#include <smbsrv/smb_fsops.h>
#include <inet/tcp.h>

static void smb_oplock_wait(smb_node_t *);

/*
 *	Magic		0xFF 'S' 'M' 'B'
 *	smb_com 	a byte, the "first" command
 *	Error		a 4-byte union, ignored in a request
 *	smb_flg		a one byte set of eight flags
 *	smb_flg2	a two byte set of 16 flags
 *	.		twelve reserved bytes, have a role
 *			in connectionless transports (IPX, UDP?)
 *	smb_tid		a 16-bit tree ID, a mount point sorta,
 *			0xFFFF is this command does not have
 *			or require a tree context
 *	smb_pid		a 16-bit process ID
 *	smb_uid		a 16-bit user ID, specific to this "session"
 *			and mapped to a system (bona-fide) UID
 *	smb_mid		a 16-bit multiplex ID, used to differentiate
 *			multiple simultaneous requests from the same
 *			process (pid) (ref RPC "xid")
 *
 * SMB_COM_LOCKING_ANDX allows both locking and/or unlocking of file range(s).
 *
 *  Client Request                     Description
 *  ================================== =================================
 *
 *  UCHAR WordCount;                   Count of parameter words = 8
 *  UCHAR AndXCommand;                 Secondary (X) command;  0xFF = none
 *  UCHAR AndXReserved;                Reserved (must be 0)
 *  USHORT AndXOffset;                 Offset to next command WordCount
 *  USHORT Fid;                        File handle
 *  UCHAR LockType;                    See LockType table below
 *  UCHAR OplockLevel;                 The new oplock level
 *  ULONG Timeout;                     Milliseconds to wait for unlock
 *  USHORT NumberOfUnlocks;            Num. unlock range structs following
 *  USHORT NumberOfLocks;              Num. lock range structs following
 *  USHORT ByteCount;                  Count of data bytes
 *  LOCKING_ANDX_RANGE Unlocks[];      Unlock ranges
 *  LOCKING_ANDX_RANGE Locks[];        Lock ranges
 *
 *  LockType Flag Name            Value Description
 *  ============================  ===== ================================
 *
 *  LOCKING_ANDX_SHARED_LOCK      0x01  Read-only lock
 *  LOCKING_ANDX_OPLOCK_RELEASE   0x02  Oplock break notification
 *  LOCKING_ANDX_CHANGE_LOCKTYPE  0x04  Change lock type
 *  LOCKING_ANDX_CANCEL_LOCK      0x08  Cancel outstanding request
 *  LOCKING_ANDX_LARGE_FILES      0x10  Large file locking format
 *
 *  LOCKING_ANDX_RANGE Format
 *  =====================================================================
 *
 *  USHORT Pid;                        PID of process "owning" lock
 *  ULONG Offset;                      Offset to bytes to [un]lock
 *  ULONG Length;                      Number of bytes to [un]lock
 *
 *  Large File LOCKING_ANDX_RANGE Format
 *  =====================================================================
 *
 *  USHORT Pid;                        PID of process "owning" lock
 *  USHORT Pad;                        Pad to DWORD align (mbz)
 *  ULONG OffsetHigh;                  Offset to bytes to [un]lock
 *                                      (high)
 *  ULONG OffsetLow;                   Offset to bytes to [un]lock (low)
 *  ULONG LengthHigh;                  Number of bytes to [un]lock
 *                                      (high)
 *  ULONG LengthLow;                   Number of bytes to [un]lock (low)
 *
 *  Server Response                    Description
 *  ================================== =================================
 *
 *  UCHAR WordCount;                   Count of parameter words = 2
 *  UCHAR AndXCommand;                 Secondary (X) command;  0xFF =
 *                                      none
 *  UCHAR AndXReserved;                Reserved (must be 0)
 *  USHORT AndXOffset;                 Offset to next command WordCount
 *  USHORT ByteCount;                  Count of data bytes = 0
 *
 */

/*
 * smb_oplock_acquire
 *
 * Attempt to acquire an oplock. Note that the oplock granted may be
 * none, i.e. the oplock was not granted. The result of the acquisition is
 * provided in ol->ol_level.
 *
 * Grant an oplock to the requestor if this session is the only one
 * that has the file open, regardless of the number of instances of
 * the file opened by this session.
 *
 * However, if there is no oplock on this file and there is already
 * at least one open, we will not grant an oplock, even if the only
 * existing opens are from the same client.  This is "server discretion."
 *
 * An oplock may need to be broken in order for one to be granted, and
 * depending on what action is taken by the other client (unlock or close),
 * an oplock may or may not be granted.  (The breaking of an oplock is
 * done earlier in the calling path.)
 */
void
smb_oplock_acquire(smb_node_t *node, smb_ofile_t *of, open_param_t *op)
{
	smb_session_t	*session;
	smb_oplock_t	*ol;
	clock_t		time;

	SMB_NODE_VALID(node);
	SMB_OFILE_VALID(of);

	ASSERT(node == SMB_OFILE_GET_NODE(of));

	session = SMB_OFILE_GET_SESSION(of);

	if (!smb_session_oplocks_enable(session) ||
	    smb_tree_has_feature(SMB_OFILE_GET_TREE(of), SMB_TREE_NO_OPLOCKS)) {
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	}

	ol = &node->n_oplock;
	time = MSEC_TO_TICK(smb_oplock_timeout) + ddi_get_lbolt();

	mutex_enter(&node->n_mutex);

	switch (node->n_state) {
	case SMB_NODE_STATE_OPLOCK_GRANTED:
		if (SMB_SESSION_GET_ID(session) == ol->ol_sess_id) {
			mutex_exit(&node->n_mutex);
			return;
		}
		break;
	case SMB_NODE_STATE_AVAILABLE:
	case SMB_NODE_STATE_OPLOCK_BREAKING:
		break;
	default:
		SMB_PANIC();
	}

	for (;;) {
		int	rc;

		smb_oplock_wait(node);

		if (node->n_state == SMB_NODE_STATE_AVAILABLE) {
			if ((op->op_oplock_level == SMB_OPLOCK_LEVEL_II) ||
			    (op->op_oplock_level == SMB_OPLOCK_NONE) ||
			    (node->n_open_count > 1)) {
				mutex_exit(&node->n_mutex);
				op->op_oplock_level = SMB_OPLOCK_NONE;
				return;
			}
			ol->ol_ofile = of;
			ol->ol_sess_id = SMB_SESSION_GET_ID(session);
			ol->ol_level = op->op_oplock_level;
			ol->ol_xthread = curthread;
			node->n_state = SMB_NODE_STATE_OPLOCK_GRANTED;
			mutex_exit(&node->n_mutex);
			if (smb_fsop_oplock_install(node, of->f_mode) == 0) {
				smb_ofile_set_oplock_granted(of);
				return;
			}
			mutex_enter(&node->n_mutex);
			ASSERT(node->n_state == SMB_NODE_STATE_OPLOCK_GRANTED);
			node->n_state = SMB_NODE_STATE_AVAILABLE;
			ol->ol_xthread = NULL;
			op->op_oplock_level = SMB_OPLOCK_NONE;
			cv_broadcast(&ol->ol_cv);
			break;
		}

		if (node->n_state == SMB_NODE_STATE_OPLOCK_GRANTED) {
			if (SMB_SESSION_GET_ID(session) == ol->ol_sess_id)
				break;
			node->n_state = SMB_NODE_STATE_OPLOCK_BREAKING;
			mutex_exit(&node->n_mutex);
			smb_session_oplock_break(
			    SMB_OFILE_GET_SESSION(ol->ol_ofile), ol->ol_ofile);
			mutex_enter(&node->n_mutex);
			continue;
		}

		ASSERT(node->n_state == SMB_NODE_STATE_OPLOCK_BREAKING);

		rc = cv_timedwait(&ol->ol_cv, &node->n_mutex, time);

		if (rc == -1) {
			/*
			 * Oplock release timed out.
			 */
			if (node->n_state == SMB_NODE_STATE_OPLOCK_BREAKING) {
				node->n_state = SMB_NODE_STATE_AVAILABLE;
				ol->ol_xthread = curthread;
				mutex_exit(&node->n_mutex);
				smb_fsop_oplock_uninstall(node);
				mutex_enter(&node->n_mutex);
				ol->ol_xthread = NULL;
				cv_broadcast(&ol->ol_cv);
			}
		}
	}
	mutex_exit(&node->n_mutex);
}

/*
 * smb_oplock_break
 *
 * The oplock break may succeed for multiple reasons: file close, oplock
 * release, holder connection dropped, requesting client disconnect etc.
 *
 * Returns:
 *
 *	B_TRUE	The oplock is broken.
 *	B_FALSE	The oplock is being broken. This is returned if nowait is set
 *		to B_TRUE;
 */
boolean_t
smb_oplock_break(smb_node_t *node, smb_session_t *session, boolean_t nowait)
{
	smb_oplock_t	*ol;
	clock_t		time;

	SMB_NODE_VALID(node);
	ol = &node->n_oplock;
	time = MSEC_TO_TICK(smb_oplock_timeout) + ddi_get_lbolt();

	if (session != NULL) {
		mutex_enter(&node->n_mutex);
		if (SMB_SESSION_GET_ID(session) == ol->ol_sess_id) {
			mutex_exit(&node->n_mutex);
			return (B_TRUE);
		}
	} else {
		mutex_enter(&node->n_mutex);
	}

	for (;;) {
		int	rc;

		smb_oplock_wait(node);

		if (node->n_state == SMB_NODE_STATE_AVAILABLE) {
			mutex_exit(&node->n_mutex);
			return (B_TRUE);
		}

		if (node->n_state == SMB_NODE_STATE_OPLOCK_GRANTED) {
			node->n_state = SMB_NODE_STATE_OPLOCK_BREAKING;
			mutex_exit(&node->n_mutex);
			smb_session_oplock_break(
			    SMB_OFILE_GET_SESSION(ol->ol_ofile), ol->ol_ofile);
			mutex_enter(&node->n_mutex);
			continue;
		}

		ASSERT(node->n_state == SMB_NODE_STATE_OPLOCK_BREAKING);
		if (nowait) {
			mutex_exit(&node->n_mutex);
			return (B_FALSE);
		}
		rc = cv_timedwait(&ol->ol_cv, &node->n_mutex, time);
		if (rc == -1) {
			/*
			 * Oplock release timed out.
			 */
			if (node->n_state == SMB_NODE_STATE_OPLOCK_BREAKING) {
				node->n_state = SMB_NODE_STATE_AVAILABLE;
				ol->ol_xthread = curthread;
				mutex_exit(&node->n_mutex);
				smb_fsop_oplock_uninstall(node);
				mutex_enter(&node->n_mutex);
				ol->ol_xthread = NULL;
				cv_broadcast(&ol->ol_cv);
				break;
			}
		}
	}
	mutex_exit(&node->n_mutex);
	return (B_TRUE);
}

/*
 * smb_oplock_release
 *
 * This function releases the oplock on the node passed in. If other threads
 * were waiting for the oplock to be released they are signaled.
 */
void
smb_oplock_release(smb_node_t *node, smb_ofile_t *of)
{
	smb_oplock_t	*ol;

	SMB_NODE_VALID(node);
	ol = &node->n_oplock;

	mutex_enter(&node->n_mutex);
	smb_oplock_wait(node);
	switch (node->n_state) {
	case SMB_NODE_STATE_AVAILABLE:
		break;

	case SMB_NODE_STATE_OPLOCK_GRANTED:
	case SMB_NODE_STATE_OPLOCK_BREAKING:
		if (ol->ol_ofile == of) {
			node->n_state = SMB_NODE_STATE_AVAILABLE;
			ol->ol_xthread = curthread;
			mutex_exit(&node->n_mutex);
			smb_fsop_oplock_uninstall(node);
			mutex_enter(&node->n_mutex);
			ol->ol_xthread = NULL;
			cv_broadcast(&ol->ol_cv);
		}
		break;

	default:
		SMB_PANIC();
	}
	mutex_exit(&node->n_mutex);
}

/*
 * smb_oplock_conflict
 *
 * The two checks on "session" and "op" are primarily for the open path.
 * Other SMB functions may call smb_oplock_conflict() with a session
 * pointer so as to do the session check.
 */
boolean_t
smb_oplock_conflict(smb_node_t *node, smb_session_t *session, open_param_t *op)
{
	boolean_t	rb;

	SMB_NODE_VALID(node);
	SMB_SESSION_VALID(session);

	mutex_enter(&node->n_mutex);
	smb_oplock_wait(node);
	switch (node->n_state) {
	case SMB_NODE_STATE_AVAILABLE:
		rb = B_FALSE;
		break;

	case SMB_NODE_STATE_OPLOCK_GRANTED:
	case SMB_NODE_STATE_OPLOCK_BREAKING:
		if (SMB_SESSION_GET_ID(session) == node->n_oplock.ol_sess_id) {
			rb = B_FALSE;
			break;
		}

		if (op != NULL) {
			if (((op->desired_access & ~(FILE_READ_ATTRIBUTES |
			    FILE_WRITE_ATTRIBUTES | SYNCHRONIZE)) == 0) &&
			    (op->create_disposition != FILE_SUPERSEDE) &&
			    (op->create_disposition != FILE_OVERWRITE)) {
				/* Attributs only */
				rb = B_FALSE;
				break;
			}
		}
		rb = B_TRUE;
		break;

	default:
		SMB_PANIC();
	}
	mutex_exit(&node->n_mutex);
	return (rb);
}

/*
 * smb_oplock_broadcast
 *
 * The the calling thread has the pointer to its context stored in ol_thread
 * it resets that field. If any other thread is waiting for that field to
 * turn to NULL it is signaled.
 *
 * Returns:
 *	B_TRUE	Oplock unlocked
 *	B_FALSE	Oplock still locked
 */
boolean_t
smb_oplock_broadcast(smb_node_t *node)
{
	smb_oplock_t	*ol;
	boolean_t	rb;

	SMB_NODE_VALID(node);
	ol = &node->n_oplock;
	rb = B_FALSE;

	mutex_enter(&node->n_mutex);
	if ((ol->ol_xthread != NULL) && (ol->ol_xthread == curthread)) {
		ol->ol_xthread = NULL;
		cv_broadcast(&ol->ol_cv);
		rb = B_TRUE;
	}
	mutex_exit(&node->n_mutex);
	return (rb);
}

/*
 * smb_oplock_wait
 *
 * The mutex of the node must have been entered before calling this function.
 * If the field ol_xthread is not NULL and doesn't contain the pointer to the
 * context of the calling thread, the caller will sleep until that field is
 * reset (set to NULL).
 */
static void
smb_oplock_wait(smb_node_t *node)
{
	smb_oplock_t	*ol = &node->n_oplock;

	if ((ol->ol_xthread != NULL) && (ol->ol_xthread != curthread)) {
		ASSERT(!MUTEX_HELD(&ol->ol_ofile->f_mutex));
		while (ol->ol_xthread != NULL)
			cv_wait(&ol->ol_cv, &node->n_mutex);
	}
}