/*
   Unix SMB/CIFS implementation.
   change notify handling - hash based implementation
   Copyright (C) Jeremy Allison 1994-1998
   Copyright (C) Andrew Tridgell 2000
   Copyright (C) Volker Lendecke 2007

   This program 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 2 of the License, or
   (at your option) any later version.

   This program 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 this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "includes.h"

struct hash_change_data {
	time_t last_check_time; /* time we last checked this entry */
	struct timespec modify_time; /* Info from the directory we're
				      * monitoring. */
	struct timespec status_time; /* Info from the directory we're
				      * monitoring. */
	time_t total_time; /* Total time of all directory entries - don't care
			    * if it wraps. */
	unsigned int num_entries; /* Zero or the number of files in the
				   * directory. */
	unsigned int mode_sum;
	unsigned char name_hash[16];
};

struct hash_notify_ctx {
	struct hash_change_data *data;
	files_struct *fsp;
	char *path;
	uint32 filter;
};

/* Compare struct timespec. */
#define TIMESTAMP_NEQ(x, y) (((x).tv_sec != (y).tv_sec) || ((x).tv_nsec != (y).tv_nsec))

/****************************************************************************
 Create the hash we will use to determine if the contents changed.
*****************************************************************************/

static BOOL notify_hash(connection_struct *conn, char *path, uint32 flags, 
			struct hash_change_data *data,
			struct hash_change_data *old_data)
{
	SMB_STRUCT_STAT st;
	pstring full_name;
	char *p;
	const char *fname;
	size_t remaining_len;
	size_t fullname_len;
	struct smb_Dir *dp;
	long offset;

	ZERO_STRUCTP(data);

	if(SMB_VFS_STAT(conn,path, &st) == -1)
		return False;

	data->modify_time = get_mtimespec(&st);
	data->status_time = get_ctimespec(&st);

	if (old_data) {
		/*
		 * Shortcut to avoid directory scan if the time
		 * has changed - we always must return true then.
		 */
		if (TIMESTAMP_NEQ(old_data->modify_time, data->modify_time) ||
		    TIMESTAMP_NEQ(old_data->status_time, data->status_time) ) {
				return True;
		}
	}
 
        if (S_ISDIR(st.st_mode) && 
            (flags & ~(FILE_NOTIFY_CHANGE_FILE_NAME
		       | FILE_NOTIFY_CHANGE_DIR_NAME)) == 0)
        {
		/* This is the case of a client wanting to know only when
		 * the contents of a directory changes. Since any file
		 * creation, rename or deletion will update the directory
		 * timestamps, we don't need to create a hash.
		 */
                return True;
        }

	/*
	 * If we are to watch for changes that are only stored
	 * in inodes of files, not in the directory inode, we must
	 * scan the directory and produce a unique identifier with
	 * which we can determine if anything changed. We use the
	 * modify and change times from all the files in the
	 * directory, added together (ignoring wrapping if it's
	 * larger than the max time_t value).
	 */

	dp = OpenDir(conn, path, NULL, 0);
	if (dp == NULL)
		return False;

	data->num_entries = 0;
	
	pstrcpy(full_name, path);
	pstrcat(full_name, "/");
	
	fullname_len = strlen(full_name);
	remaining_len = sizeof(full_name) - fullname_len - 1;
	p = &full_name[fullname_len];
	
	offset = 0;
	while ((fname = ReadDirName(dp, &offset))) {
		SET_STAT_INVALID(st);
		if(strequal(fname, ".") || strequal(fname, ".."))
			continue;		

		if (!is_visible_file(conn, path, fname, &st, True))
			continue;

		data->num_entries++;
		safe_strcpy(p, fname, remaining_len);

		/*
		 * Do the stat - but ignore errors.
		 */		
		if (!VALID_STAT(st)) {
			SMB_VFS_STAT(conn,full_name, &st);
		}

		/*
		 * Always sum the times.
		 */

		data->total_time += (st.st_mtime + st.st_ctime);

		/*
		 * If requested hash the names.
		 */

		if (flags & (FILE_NOTIFY_CHANGE_DIR_NAME
			     |FILE_NOTIFY_CHANGE_FILE_NAME
			     |FILE_NOTIFY_CHANGE_FILE)) {
			int i;
			unsigned char tmp_hash[16];
			mdfour(tmp_hash, (const unsigned char *)fname,
			       strlen(fname));
			for (i=0;i<16;i++)
				data->name_hash[i] ^= tmp_hash[i];
		}

		/*
		 * If requested sum the mode_t's.
		 */

		if (flags & (FILE_NOTIFY_CHANGE_ATTRIBUTES
			     |FILE_NOTIFY_CHANGE_SECURITY))
			data->mode_sum += st.st_mode;
	}
	
	CloseDir(dp);
	
	return True;
}

static void hash_change_notify_handler(struct event_context *event_ctx,
				       struct timed_event *te,
				       const struct timeval *now,
				       void *private_data)
{
	struct hash_change_data *new_data;
	struct hash_notify_ctx *ctx =
		talloc_get_type_abort(private_data,
				      struct hash_notify_ctx);

	TALLOC_FREE(te);

	if (!(new_data = TALLOC_P(ctx, struct hash_change_data))) {
		DEBUG(0, ("talloc failed\n"));
		/*
		 * No new timed event;
		 */
		return;
	}

	if (!notify_hash(ctx->fsp->conn, ctx->fsp->fsp_name,
			 ctx->filter, new_data, ctx->data)
	    || TIMESTAMP_NEQ(new_data->modify_time, ctx->data->modify_time)
	    || TIMESTAMP_NEQ(new_data->status_time, ctx->data->status_time)
	    || new_data->total_time != ctx->data->total_time
	    || new_data->num_entries != ctx->data->num_entries
	    || new_data->mode_sum != ctx->data->mode_sum
	    || (memcmp(new_data->name_hash, ctx->data->name_hash,
		       sizeof(new_data->name_hash)))) {
		notify_fsp(ctx->fsp, 0, NULL);
	}

	TALLOC_FREE(ctx->data);
	ctx->data = new_data;

        event_add_timed(
		event_ctx, ctx,
		timeval_current_ofs(
			lp_change_notify_timeout(SNUM(ctx->fsp->conn)), 0),
		"hash_change_notify_handler",
		hash_change_notify_handler, ctx);
}



static void *hash_notify_add(TALLOC_CTX *mem_ctx,
			     struct event_context *event_ctx,
			     files_struct *fsp,
			     uint32 *filter)
{
	struct hash_notify_ctx *ctx;
	int timeout = lp_change_notify_timeout(SNUM(fsp->conn));
	pstring fullpath;

	if (timeout <= 0) {
		/* It change notify timeout has been disabled, never scan the
		 * directory. */
		return NULL;
	}

	pstrcpy(fullpath, fsp->fsp_name);
	if (!canonicalize_path(fsp->conn, fullpath)) {
		DEBUG(0, ("failed to canonicalize path '%s'\n", fullpath));
		return NULL;
	}

	if (*fullpath != '/') {
		DEBUG(0, ("canonicalized path '%s' into `%s`\n", fsp->fsp_name,
			  fullpath));
		DEBUGADD(0, ("but expected an absolute path\n"));
		return NULL;
	}
	
	if (!(ctx = TALLOC_P(mem_ctx, struct hash_notify_ctx))) {
		DEBUG(0, ("talloc failed\n"));
		return NULL;
	}

	if (!(ctx->path = talloc_strdup(ctx, fullpath))) {
		DEBUG(0, ("talloc_strdup failed\n"));
		TALLOC_FREE(ctx);
		return NULL;
	}

	if (!(ctx->data = TALLOC_P(ctx, struct hash_change_data))) {
		DEBUG(0, ("talloc failed\n"));
		TALLOC_FREE(ctx);
		return NULL;
	}

	ctx->fsp = fsp;

	/*
	 * Don't change the Samba filter, hash can only be a very bad attempt
	 * anyway.
	 */
	ctx->filter = *filter;

	notify_hash(fsp->conn, ctx->path, ctx->filter, ctx->data, NULL);

	event_add_timed(event_ctx, ctx, timeval_current_ofs(timeout, 0),
			"hash_change_notify_handler",
			hash_change_notify_handler, ctx);

	return (void *)ctx;
}

/****************************************************************************
 Setup hash based change notify.
****************************************************************************/

struct cnotify_fns *hash_notify_init(void) 
{
	static struct cnotify_fns cnotify;

	cnotify.notify_add = hash_notify_add;

	return &cnotify;
}