/*
 * 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 (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2013 Nexenta Systems, Inc.  All rights reserved.
 */

/*
 * File Change Notification (FCN)
 * Common parts shared by SMB1 & SMB2
 */

/*
 * This command notifies the client when the specified directory
 * has changed, and optionally returns the names of files and
 * directories that changed, and how they changed.  The caller
 * specifies a "Completion Filter" to select which kinds of
 * changes they want to know about.
 *
 * When a change that's in the CompletionFilter is made to the directory,
 * the command completes.  The names of the files that have changed since
 * the last time the command was issued are returned to the client.
 * If too many files have changed since the last time the command was
 * issued, then zero bytes are returned and an alternate status code
 * is returned in the Status field of the response.
 *
 * The CompletionFilter is a mask created as the sum of any of the
 * following flags:
 *
 * FILE_NOTIFY_CHANGE_FILE_NAME        0x00000001
 * FILE_NOTIFY_CHANGE_DIR_NAME         0x00000002
 * FILE_NOTIFY_CHANGE_NAME             0x00000003
 * FILE_NOTIFY_CHANGE_ATTRIBUTES       0x00000004
 * FILE_NOTIFY_CHANGE_SIZE             0x00000008
 * FILE_NOTIFY_CHANGE_LAST_WRITE       0x00000010
 * FILE_NOTIFY_CHANGE_LAST_ACCESS      0x00000020
 * FILE_NOTIFY_CHANGE_CREATION         0x00000040
 * FILE_NOTIFY_CHANGE_EA               0x00000080
 * FILE_NOTIFY_CHANGE_SECURITY         0x00000100
 * FILE_NOTIFY_CHANGE_STREAM_NAME      0x00000200
 * FILE_NOTIFY_CHANGE_STREAM_SIZE      0x00000400
 * FILE_NOTIFY_CHANGE_STREAM_WRITE     0x00000800
 *
 *
 * The response contains FILE_NOTIFY_INFORMATION structures, as defined
 * below.  The NextEntryOffset field of the structure specifies the offset,
 * in bytes, from the start of the current entry to the next entry in the
 * list.  If this is the last entry in the list, this field is zero.  Each
 * entry in the list must be longword aligned, so NextEntryOffset must be a
 * multiple of four.
 *
 * typedef struct {
 *     ULONG NextEntryOffset;
 *     ULONG Action;
 *     ULONG FileNameLength;
 *     WCHAR FileName[1];
 * } FILE_NOTIFY_INFORMATION;
 *
 * Where Action describes what happened to the file named FileName:
 *
 * FILE_ACTION_ADDED            0x00000001
 * FILE_ACTION_REMOVED          0x00000002
 * FILE_ACTION_MODIFIED         0x00000003
 * FILE_ACTION_RENAMED_OLD_NAME 0x00000004
 * FILE_ACTION_RENAMED_NEW_NAME 0x00000005
 * FILE_ACTION_ADDED_STREAM     0x00000006
 * FILE_ACTION_REMOVED_STREAM   0x00000007
 * FILE_ACTION_MODIFIED_STREAM  0x00000008
 */

#include <smbsrv/smb_kproto.h>
#include <sys/sdt.h>

static void smb_notify_sr(smb_request_t *, uint_t, const char *);
static uint32_t smb_notify_encode_action(struct smb_request *,
	mbuf_chain_t *, uint32_t, char *);

uint32_t
smb_notify_common(smb_request_t *sr, mbuf_chain_t *mbc,
	uint32_t CompletionFilter)
{
	smb_notify_change_req_t *nc;
	smb_node_t	*node;
	uint32_t	status;

	if (sr->fid_ofile == NULL)
		return (NT_STATUS_INVALID_HANDLE);

	node = sr->fid_ofile->f_node;
	if (node == NULL || !smb_node_is_dir(node)) {
		/*
		 * Notify change is only valid on directories.
		 */
		return (NT_STATUS_INVALID_PARAMETER);
	}

	/*
	 * Prepare to receive event data.
	 */
	nc = &sr->sr_ncr;
	nc->nc_flags = CompletionFilter;
	ASSERT(nc->nc_action == 0);
	ASSERT(nc->nc_fname == NULL);
	nc->nc_fname = kmem_zalloc(MAXNAMELEN, KM_SLEEP);

	/*
	 * Subscribe to events on this node.
	 */
	smb_node_fcn_subscribe(node, sr);

	/*
	 * Wait for subscribed events to arrive.
	 * Expect SMB_REQ_STATE_EVENT_OCCURRED
	 * or SMB_REQ_STATE_CANCELED when signaled.
	 * Note it's possible (though rare) to already
	 * have SMB_REQ_STATE_CANCELED here.
	 */
	mutex_enter(&sr->sr_mutex);
	if (sr->sr_state == SMB_REQ_STATE_ACTIVE)
		sr->sr_state = SMB_REQ_STATE_WAITING_EVENT;
	while (sr->sr_state == SMB_REQ_STATE_WAITING_EVENT) {
		cv_wait(&nc->nc_cv, &sr->sr_mutex);
	}
	if (sr->sr_state == SMB_REQ_STATE_EVENT_OCCURRED)
		sr->sr_state = SMB_REQ_STATE_ACTIVE;
	mutex_exit(&sr->sr_mutex);

	/*
	 * Unsubscribe from events on this node.
	 */
	smb_node_fcn_unsubscribe(node, sr);

	/*
	 * Why did we wake up?
	 */
	switch (sr->sr_state) {
	case SMB_REQ_STATE_ACTIVE:
		break;
	case SMB_REQ_STATE_CANCELED:
		status = NT_STATUS_CANCELLED;
		goto out;
	default:
		status = NT_STATUS_INTERNAL_ERROR;
		goto out;
	}

	/*
	 * We have SMB_REQ_STATE_ACTIVE.
	 *
	 * If we have event data, marshall it now, else just
	 * say "many things changed". Note that when we get
	 * action FILE_ACTION_SUBDIR_CHANGED, we don't have
	 * any event details and only know that some subdir
	 * changed, so just report "many things changed".
	 */
	switch (nc->nc_action) {

	case FILE_ACTION_ADDED:
	case FILE_ACTION_REMOVED:
	case FILE_ACTION_MODIFIED:
	case FILE_ACTION_RENAMED_OLD_NAME:
	case FILE_ACTION_RENAMED_NEW_NAME:
	case FILE_ACTION_ADDED_STREAM:
	case FILE_ACTION_REMOVED_STREAM:
	case FILE_ACTION_MODIFIED_STREAM:
		/*
		 * Build the reply
		 */
		status = smb_notify_encode_action(sr, mbc,
		    nc->nc_action, nc->nc_fname);
		break;

	case FILE_ACTION_SUBDIR_CHANGED:
		status = NT_STATUS_NOTIFY_ENUM_DIR;
		break;

	case FILE_ACTION_DELETE_PENDING:
		status = NT_STATUS_DELETE_PENDING;
		break;

	default:
		ASSERT(0);
		status = NT_STATUS_INTERNAL_ERROR;
		break;
	}

out:
	kmem_free(nc->nc_fname, MAXNAMELEN);
	nc->nc_fname = NULL;
	return (status);
}

/*
 * Encode a FILE_NOTIFY_INFORMATION struct.
 *
 * We only ever put one of these in a response, so this
 * does not bother handling appending additional ones.
 */
static uint32_t
smb_notify_encode_action(struct smb_request *sr, mbuf_chain_t *mbc,
	uint32_t action, char *fname)
{
	uint32_t namelen;

	ASSERT(FILE_ACTION_ADDED <= action &&
	    action <= FILE_ACTION_MODIFIED_STREAM);

	if (fname == NULL)
		return (NT_STATUS_INTERNAL_ERROR);
	namelen = smb_wcequiv_strlen(fname);
	if (namelen == 0)
		return (NT_STATUS_INTERNAL_ERROR);

	if (smb_mbc_encodef(mbc, "%lllU", sr,
	    0, /* NextEntryOffset */
	    action, namelen, fname))
		return (NT_STATUS_NOTIFY_ENUM_DIR);

	return (0);
}

/*
 * smb_notify_file_closed
 *
 * Cancel any change-notify calls on this open file.
 */
void
smb_notify_file_closed(struct smb_ofile *of)
{
	smb_session_t	*ses;
	smb_request_t	*sr;
	smb_slist_t	*list;

	SMB_OFILE_VALID(of);
	ses = of->f_session;
	SMB_SESSION_VALID(ses);
	list = &ses->s_req_list;

	smb_slist_enter(list);

	sr = smb_slist_head(list);
	while (sr) {
		SMB_REQ_VALID(sr);
		if (sr->sr_state == SMB_REQ_STATE_WAITING_EVENT &&
		    sr->fid_ofile == of) {
			smb_request_cancel(sr);
		}
		sr = smb_slist_next(list, sr);
	}

	smb_slist_exit(list);
}


/*
 * smb_notify_event
 *
 * Post an event to the watchers on a given node.
 *
 * This makes one exception for RENAME, where we expect a
 * pair of events for the {old,new} directory element names.
 * This only delivers an event for the "new" name.
 *
 * The event delivery mechanism does not implement delivery of
 * multiple events for one "NT Notify" call.  One could do that,
 * but modern clients don't actually use the event data.  They
 * set a max. received data size of zero, which means we discard
 * the data and send the special "lots changed" error instead.
 * Given that, there's not really any point in implementing the
 * delivery of multiple events.  In fact, we don't even need to
 * implement single event delivery, but do so for completeness,
 * for debug convenience, and to be nice to older clients that
 * may actually want some event data instead of the error.
 *
 * Given that we only deliver a single event for an "NT Notify"
 * caller, we want to deliver the "new" name event.  (The "old"
 * name event is less important, even ignored by some clients.)
 * Since we know these are delivered in pairs, we can simply
 * discard the "old" name event, knowing that the "new" name
 * event will be delivered immediately afterwards.
 *
 * So, why do event sources post the "old name" event at all?
 * (1) For debugging, so we see both {old,new} names here.
 * (2) If in the future someone decides to implement the
 * delivery of both {old,new} events, the changes can be
 * mostly isolated to this file.
 */
void
smb_notify_event(smb_node_t *node, uint_t action, const char *name)
{
	smb_request_t	*sr;
	smb_node_fcn_t	*fcn;

	SMB_NODE_VALID(node);
	fcn = &node->n_fcn;

	if (action == FILE_ACTION_RENAMED_OLD_NAME)
		return; /* see above */

	mutex_enter(&fcn->fcn_mutex);

	sr = list_head(&fcn->fcn_watchers);
	while (sr) {
		smb_notify_sr(sr, action, name);
		sr = list_next(&fcn->fcn_watchers, sr);
	}

	mutex_exit(&fcn->fcn_mutex);
}

/*
 * What completion filter (masks) apply to each of the
 * FILE_ACTION_... events.
 */
static const uint32_t
smb_notify_action_mask[] = {
	0,  /* not used */

	/* FILE_ACTION_ADDED	 */
	FILE_NOTIFY_CHANGE_NAME |
	FILE_NOTIFY_CHANGE_LAST_WRITE,

	/* FILE_ACTION_REMOVED	 */
	FILE_NOTIFY_CHANGE_NAME |
	FILE_NOTIFY_CHANGE_LAST_WRITE,

	/* FILE_ACTION_MODIFIED	 */
	FILE_NOTIFY_CHANGE_ATTRIBUTES |
	FILE_NOTIFY_CHANGE_SIZE |
	FILE_NOTIFY_CHANGE_LAST_WRITE |
	FILE_NOTIFY_CHANGE_LAST_ACCESS |
	FILE_NOTIFY_CHANGE_CREATION |
	FILE_NOTIFY_CHANGE_EA |
	FILE_NOTIFY_CHANGE_SECURITY,

	/* FILE_ACTION_RENAMED_OLD_NAME */
	FILE_NOTIFY_CHANGE_NAME |
	FILE_NOTIFY_CHANGE_LAST_WRITE,

	/* FILE_ACTION_RENAMED_NEW_NAME */
	FILE_NOTIFY_CHANGE_NAME |
	FILE_NOTIFY_CHANGE_LAST_WRITE,

	/* FILE_ACTION_ADDED_STREAM */
	FILE_NOTIFY_CHANGE_STREAM_NAME,

	/* FILE_ACTION_REMOVED_STREAM */
	FILE_NOTIFY_CHANGE_STREAM_NAME,

	/* FILE_ACTION_MODIFIED_STREAM */
	FILE_NOTIFY_CHANGE_STREAM_SIZE |
	FILE_NOTIFY_CHANGE_STREAM_WRITE,

	/* FILE_ACTION_SUBDIR_CHANGED */
	NODE_FLAGS_WATCH_TREE,

	/* FILE_ACTION_DELETE_PENDING */
	NODE_FLAGS_WATCH_TREE |
	FILE_NOTIFY_VALID_MASK,
};
static const int smb_notify_action_nelm =
	sizeof (smb_notify_action_mask) /
	sizeof (smb_notify_action_mask[0]);

/*
 * smb_notify_sr
 *
 * Post an event to an smb request waiting on some node.
 *
 * Note that node->fcn.mutex is held.  This implies a
 * lock order: node->fcn.mutex, then sr_mutex
 */
static void
smb_notify_sr(smb_request_t *sr, uint_t action, const char *name)
{
	smb_notify_change_req_t	*ncr;
	uint32_t	mask;

	SMB_REQ_VALID(sr);
	ncr = &sr->sr_ncr;

	/*
	 * Compute the completion filter mask bits for which
	 * we will signal waiting notify requests.
	 */
	VERIFY(action < smb_notify_action_nelm);
	mask = smb_notify_action_mask[action];

	mutex_enter(&sr->sr_mutex);
	if (sr->sr_state == SMB_REQ_STATE_WAITING_EVENT &&
	    (ncr->nc_flags & mask) != 0) {
		sr->sr_state = SMB_REQ_STATE_EVENT_OCCURRED;
		/*
		 * Save event data in the sr_ncr field so the
		 * reply handler can return it.
		 */
		ncr->nc_action = action;
		if (name != NULL)
			(void) strlcpy(ncr->nc_fname, name, MAXNAMELEN);
		cv_signal(&ncr->nc_cv);
	}
	mutex_exit(&sr->sr_mutex);
}