/*
 * Name: smb_abstraction.c
 * Description: Smb abstraction layer.
 * Author: Christian Starkjohann <cs@hal.kph.tuwien.ac.at>
 * Date: 1996-12-31
 * Copyright: GNU-GPL
 * Tabsize: 4
 */

#include "syshdr.h"
#include <sys/stat.h>
#include <sys/socket.h>
#include <ctype.h>
#include <netdb.h>
#include <smb/smb.h>
#include <smb/smb_fs.h>
#include <smb/smbno.h>
#include "psinode.h"
#include "my_defines.h"

/*#define	DPRINTF(arg) debprintf arg*/
#define	DPRINTF(arg)

/* ------------------------------------------------------------------------- */

#define	ATTR_CACHE_TIME		5	/* cache attributes for this time */
#define	DIR_CACHE_TIME		5	/* cache directories for this time */
#define	DIRCACHE_SIZE		170
#define	DOS_PATHSEP			'\\'

/* ------------------------------------------------------------------------- */

extern int	smb_proc_setattrE(struct smb_server *server, word fid,
                  							struct smb_dirent *new_entry);
extern int	smb_proc_setattr_core(struct smb_server *server, const char *path,
									int len, struct smb_dirent *new_finfo);
extern int	smb_proc_getattrE(struct smb_server *server,
													struct smb_dirent *entry);
extern int	smb_proc_getattr_core(struct smb_server *server, const char *path,
										int len, struct smb_dirent *entry);


/* ------------------------------------------------------------------------- */

typedef struct dircache{
	struct smb_dirent	cache[DIRCACHE_SIZE];
	int					base;
	int					len;
	int					eof;			/* cache end is eof */
	long				created_at;		/* for invalidation */
	struct smba_file	*cache_for;		/* owner of this cache */
}dircache_t;

/* opaque structures for server and files: */
struct smba_server{
	struct smba_server	*next;
	struct smba_server	**prevnext;		/* doubly linked list */
	struct smb_server	server;
	list_t				open_files;
	unsigned			supports_E			:1;
	unsigned			supports_E_known	:1;
};

struct smba_file{
	struct smba_file	*next;		/* doubly linked list of open files */
	struct smba_file	**prevnext;
	struct smba_server	*server;
	struct smb_dirent	dirent;
	long				attr_time;	/* time when dirent was read */
	char				attr_dirty;	/* attribute cache is dirty */
	char				is_valid;	/* server was down, entry removed, ... */
	dircache_t			*dircache;	/* content cache for directories */
};

#include "smb_abstraction.h"

static dircache_t	the_dircache;
list_t				servers;

/* ------------------------------------------------------------------------- */

static inline void	str_upper(char *name)
{
	while(*name){
		*name = toupper(*name);
		name ++;
	}
}

static inline char	*my_strdup(const char *s)
{
	return strcpy(malloc(strlen(s) + 1), s);
}

static inline int	uncase_strcmp(const char *s1, const char *s2)
{
	for(;;){
		if(*s1 == 0 && *s2 == 0)
			return 0;
		if(tolower(*s1) != tolower(*s2))
			return 1;
		s1++; s2++;
	}
}

/* ------------------------------------------------------------------------- */

#define	STRCPY(dest, src){	/* dest must be char array! */	\
		strncpy(dest, src, sizeof(dest));					\
		(dest)[sizeof(dest)-1] = 0;							\
	}


int	smba_connect(smba_connect_parameters_t *p,int use_E,smba_server_t **result)
{
smba_server_t			*res = calloc(1, sizeof(*res));
struct smb_mount_data	data;
int						errnum = -1;
char					hostname[MAXHOSTNAMELEN + 1], *s;
struct hostent			*h;

	*result = res;
	memset(&data, 0, sizeof(struct smb_mount_data));
	memset(hostname, 0, sizeof(hostname));
	gethostname(hostname, MAXHOSTNAMELEN);
	if((s = strchr(hostname, '.')) != NULL){
		*s = 0;
	}
	if((h = gethostbyname(p->server_ipname)) == NULL){
		eprintf("%s: unknown host\n", p->server_ipname);
		goto error_occured;
	}
	data.addr.sin_family = AF_INET;
	data.addr.sin_addr.s_addr = ((struct in_addr *)(h->h_addr))->s_addr;
	data.addr.sin_port = htons(p->port > 0 ? p->port : SMB_PORT);
	data.fd = socket(AF_INET, SOCK_STREAM, 0);
	if (data.fd == -1) {
		eprintf("smba_connect: socket: [%d] %s\n", errno, strerror(errno));
		goto error_occured;
	}
	data.version = SMB_MOUNT_VERSION;
	STRCPY(data.service, p->service);
	str_upper(data.service);
	STRCPY(data.root_path, p->root_path);
	STRCPY(data.username, p->username);
	STRCPY(data.password, p->password);
	if(p->max_xmit > 0)
		data.max_xmit = p->max_xmit;
	else
		data.max_xmit = 8300;
	STRCPY(data.server_name, p->server_name);
	STRCPY(data.client_name, p->client_name);

	if(data.server_name[0] == 0){
		if(strlen(p->server_ipname) > 16){
			eprintf("server name too long as a netbios name: %s\n",
															p->server_ipname);
			goto error_occured;
		}
		strcpy(data.server_name, p->server_ipname);
	}
	str_upper(data.server_name);
	if(data.client_name[0] == 0) {
		if(strlen(hostname) > 16) {
			eprintf("my hostname name too long for netbios: %s\n", hostname);
			goto error_occured;
		}
		strcpy(data.client_name, hostname);
		str_upper(data.client_name);
	}
	res->server.m = data;
	list_init(&res->open_files);

	if((errnum = smb_proc_connect(&res->server)) < 0){
		eprintf("error connecting to server: [%d] %s\n",
												-errnum, strerror(-errnum));
		goto error_occured;
	}
	res->server.case_handling = res->server.protocol >= PROTOCOL_LANMAN2 ?
												CASE_DEFAULT : CASE_LOWER;
	list_append(&servers, res);
	if(!use_E){
		res->supports_E_known = 1;
	}
	return 0;
error_occured:
	free(res);
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_disconnect(smba_server_t *server)
{
	if(server->open_files.head != NULL){
		eprintf("smba_disconnect: still files open\n");
		return -1;
	}
	list_remove_element(&servers, server);
	close(server->server.m.fd);
	free(server);
	return 0;
}

/* ------------------------------------------------------------------------- */

static inline int	make_open(smba_file_t *f, int need_fid)
{
int				errnum = 0;
smba_server_t	*s;

	if(!f->is_valid || (need_fid && !f->dirent.opened)){
		s = f->server;
		if(!f->is_valid || f->attr_time == -1
							|| time(NULL) - f->attr_time > ATTR_CACHE_TIME){
			if((errnum = smb_proc_getattr_core(&s->server, f->dirent.path,
											f->dirent.len, &f->dirent)) < 0)
				goto error_occured;
		}
		if((f->dirent.attr & aDIR) == 0){	/* a regular file */
			if(need_fid || !s->supports_E_known || s->supports_E){
				DPRINTF(("make_open(): opening file %s\n", f->dirent.path));
				if((errnum = smb_proc_open(&s->server, f->dirent.path,
											f->dirent.len, &f->dirent)) < 0)
					goto error_occured;
				if(s->supports_E || !s->supports_E_known){
					if(smb_proc_getattrE(&s->server, &f->dirent) < 0){
						if(!s->supports_E_known){
							s->supports_E_known = 1;
							s->supports_E = 0;
						}	/* ignore errors here */
					}else{
						s->supports_E_known = 1;
						s->supports_E = 1;
					}
				}
			}
		}else{	/* don't open directory, initialize directory cache */
			if(f->dircache != NULL){
				f->dircache->cache_for = NULL;
				f->dircache->len = 0;
				f->dircache = NULL;
			}
		}
		f->attr_time = time(NULL);
		f->is_valid = 1;
	}
error_occured:
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_open(smba_server_t *s, const char *name, smba_file_t **file)
{
smba_file_t	*f;
int			errnum;

	*file = f = calloc(1, sizeof(*f));
	f->dirent.path = my_strdup(name);
	f->dirent.len = strlen(name);
	f->server = s;
	if((errnum = make_open(f, 0)) < 0)
		goto error_occured;
	list_append(&s->open_files, f);
	return 0;
error_occured:
	free(f->dirent.path);
	free(f);
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_file_importance(smba_file_t *f)
{
	DPRINTF(("smba_file_importance(): file ->%s<-\n", f->dirent.path));
	if(!f->is_valid)
		return 0;
	/* entries with directory cache are most important */
	if(f->dircache != NULL)
		return 2;
	/* open entries are important, too */
	if(f->dirent.opened)
		return 1;
	return 0;
}

/* ------------------------------------------------------------------------- */

static int	write_attr(smba_file_t *f)
{
int		errnum;

	DPRINTF(("write_attr(): file %s\n", f->dirent.path));
	if((errnum = make_open(f, 0)) < 0)
		return errnum;
	if(f->dirent.opened && f->server->supports_E){
		errnum = smb_proc_setattrE(&f->server->server, f->dirent.fileid,
																&f->dirent);
	}else{
		errnum = smb_proc_setattr_core(&f->server->server, f->dirent.path,
												f->dirent.len, &f->dirent);
	}
	if(errnum < 0){
		f->attr_time = -1;
	}else{
		f->attr_dirty = 0;
	}
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_close(smba_file_t *f)
{
int		errnum = 0;

	list_remove_element(&f->server->open_files, f);
/* should not be necessary because smba_setattr() writes through
 * 	if(f->attr_dirty){
 * 		write_attr(f);
 * 	}
 */
	if(f->dirent.opened){
		DPRINTF(("smba_close(): closing file %s\n", f->dirent.path));
		errnum = smb_proc_close(&f->server->server, f->dirent.fileid,
															f->dirent.mtime);
	}
	if(f->dirent.path != NULL)
		free(f->dirent.path);
	if(f->dircache != NULL){
		f->dircache->cache_for = NULL;
		f->dircache->len = 0;
		f->dircache = NULL;
	}
	free(f);
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_read(smba_file_t *f, char *data, long len, long offset)
{
int		maxsize, count, totalcount, result = 0;
int		bytes_read = 0;
char	*rpos;

	if((result = make_open(f, 1)) < 0)
		return result;
	if(f->server->server.blkmode & 1){
		result = smb_proc_read_raw(&f->server->server, &f->dirent, offset,
																len, data);
		DPRINTF(("smb_proc_read_raw(%s)->%d\n", f->dirent.path, result));
	}
	if(result <= 0) {
		totalcount = len;
		rpos = data;
		maxsize = f->server->server.max_xmit - SMB_HEADER_LEN - 5 * 2 - 5;
		do{
			count = totalcount > maxsize ? maxsize : totalcount;
			result = smb_proc_read(&f->server->server, &f->dirent,
													offset, count, rpos, 0);
			if(result <= 0)
				break;
 			bytes_read += result;
			totalcount -= result;
			offset += result;
			rpos += result;
		}while(totalcount > 0);
	}else
		bytes_read = result;
	return bytes_read;
}

/* ------------------------------------------------------------------------- */

int	smba_write(smba_file_t *f, char *data, long len, long offset)
{
int		newlen, maxsize, totalcount, count, result = 0;

	if((result = make_open(f, 1)) < 0)
		return result;
	newlen = f->dirent.size;
	if(offset + len > newlen)
		newlen = offset + len;
	/* Added by Brian Willette - We were always checking bit 2 here, but
	 * according to the documentation I have, the newer versions of SMB put
	 * the SMB_RAW_WRITE AND SMB_RAW_READ capability in bit 1
	 */
	if((f->server->server.protocol >= PROTOCOL_NT1
					&& f->server->server.blkmode & 1)
			|| (f->server->server.protocol < PROTOCOL_NT1
					&& f->server->server.blkmode & 2)){
		result = smb_proc_write_raw(&f->server->server, &f->dirent, offset,
																len, data);
	}
	if (result <= 0){
		maxsize = f->server->server.max_xmit - SMB_HEADER_LEN - 5 * 2 - 5;
		totalcount = len;
		do{
			count = totalcount > maxsize ? maxsize : totalcount;
			result = smb_proc_write(&f->server->server, &f->dirent, offset,
																count, data);
			if(result < 0)
				break;
			totalcount -= count;
			offset += count;
			data += count;
		}while(totalcount > 0);
	}
	f->dirent.mtime = time(NULL);
	if(result < 0){
		f->attr_time = -1;
		return result;
	}
	f->dirent.size = newlen;
	return len;
}

/* ------------------------------------------------------------------------- */

int	smba_getattr(smba_file_t *f, smba_stat_t *data)
{
long	now = time(NULL);
int		errnum = 0;

	if((errnum = make_open(f, 0)) < 0)
		return errnum;
	if(f->attr_time == -1 || (now - f->attr_time) > ATTR_CACHE_TIME){
		DPRINTF(("smba_getattr(): file %s\n", f->dirent.path));
		if(f->dirent.opened && f->server->supports_E){
			errnum = smb_proc_getattrE(&f->server->server, &f->dirent);
		}else{
			errnum = smb_proc_getattr_core(&f->server->server,
								f->dirent.path, f->dirent.len, &f->dirent);
		}
		if(errnum >= 0)
			f->attr_time = now;
	}
	data->is_dir = (f->dirent.attr & aDIR) != 0;
	data->is_wp = (f->dirent.attr & aRONLY) != 0;
	data->is_hidden = (f->dirent.attr & aHIDDEN) != 0;
	data->is_system = (f->dirent.attr & aSYSTEM) != 0;
	data->is_volid = (f->dirent.attr & aVOLID) != 0;
	data->size = f->dirent.size;
	data->atime = f->dirent.atime;
	data->ctime = f->dirent.ctime;
	data->mtime = f->dirent.mtime;
	return errnum;
}

/* ------------------------------------------------------------------------- */

int smb_make_open(struct inode *i, int right)
{
	eprintf("dummy function smb_make_open() called\n");
	return -1;
}

/* ------------------------------------------------------------------------- */

int	smba_setattr(smba_file_t *f, smba_stat_t *data)
{
int errnum = 0;

	if(data->atime != -1)
		f->dirent.atime = data->atime;
	if(data->ctime != -1)
		f->dirent.ctime = data->ctime;
	if(data->mtime != -1)
		f->dirent.mtime = data->mtime;
	f->dirent.attr &= ~aRONLY;
	if(data->is_wp)
		f->dirent.attr |= aRONLY;
	f->attr_dirty = 1;
	if((errnum = write_attr(f)) != 0)
		goto error_occured;
	if(data->size != -1 && data->size != f->dirent.size){
		if((errnum = make_open(f, 1)) < 0)
			goto error_occured;
		if((errnum = smb_proc_trunc(&f->server->server, f->dirent.fileid,
															data->size)) < 0)
			goto error_occured;
		f->dirent.size = data->size;
	}
error_occured:
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_touch(smba_file_t *f)
{
int		errnum = 0;

	f->dirent.mtime = time(NULL);
	f->attr_dirty = 1;
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_name(smba_file_t *file, char **name)
{
	*name = file->dirent.path;
	return 0;
}

/* ------------------------------------------------------------------------- */

int	smba_readdir(smba_file_t *f, long offs, void *d, smba_callback_t callback)
{
struct inode			dir_ino;
struct smb_inode_info	dir_info;
int						index, rval = 0, o, eof, count = 0;
long					now = time(NULL);

	if((rval = make_open(f, 0)) < 0)
		return rval;
	if(f->dircache == NULL){							/* get a cache */
		if(the_dircache.cache_for != NULL){
			the_dircache.cache_for->dircache = NULL;	/* steal it */
		}
		the_dircache.eof = the_dircache.len = the_dircache.base = 0;
		the_dircache.cache_for = f;
		f->dircache = &the_dircache;
		DPRINTF(("smba_readdir(): stealing cache\n"));
	}else{
		if((now - f->dircache->created_at) >= DIR_CACHE_TIME){
			f->dircache->eof = f->dircache->len = f->dircache->base = 0;
			DPRINTF(("smba_readdir(): cache outdated\n"));
		}
	}
	for(index = offs;;index++){
		if(index < f->dircache->base		/* fill cache if necessary */
							|| index >= f->dircache->base + f->dircache->len){
			if(index >= f->dircache->base + f->dircache->len
														&& f->dircache->eof)
				break;	/* nothing more to read */
			DPRINTF(("smba_readdir(): cachefill for %s\n", f->dirent.path));
			DPRINTF(("\tbase was: %d, len was: %d, newbase=%d\n",
							f->dircache->base, f->dircache->len, index));
			f->dircache->len = 0;
			f->dircache->base = index;
			/* the following line is an evil hack!!! */
			dir_ino.u.generic_ip = (char *)&f->dirent -
							(char *)&dir_info.finfo + (char *)&dir_info;
			rval = smb_proc_readdir(&f->server->server, &dir_ino, index,
											DIRCACHE_SIZE, f->dircache->cache);
			if(rval <= 0)
				break;
			f->dircache->len = rval;
			f->dircache->eof = rval < DIRCACHE_SIZE;
			f->dircache->created_at = now;
			DPRINTF(("smba_readdir(): cachefill with %d entries\n", rval));
		}
		o = index - f->dircache->base;
		eof = o >= (f->dircache->len - 1) && f->dircache->eof;
		count++;
		DPRINTF(("smba_readdir(): delivering ->%s<-, index=%d, eof=%d\n",
								f->dircache->cache[o].path, index, eof));
		if((*callback)(d, index, index+1, f->dircache->cache[o].path, eof))
			break;
	}
	if(rval < 0)
		return rval;
	else
		return count;
}

/* ------------------------------------------------------------------------- */

static void	invalidate_dircache(void)
{
	the_dircache.eof = the_dircache.len = the_dircache.base = 0;
}

/* ------------------------------------------------------------------------- */

int	smba_create(smba_file_t *dir, const char *name, smba_stat_t *attr)
{
struct smb_dirent	entry;
char				*path;
int					errnum;

	if((errnum = make_open(dir, 0)) < 0)
		return errnum;
	memset(&entry, 0, sizeof(entry));
	if(attr->is_wp)
		entry.attr |= aRONLY;
	entry.atime = entry.mtime = entry.ctime = time(NULL);
	path = malloc(strlen(name) + dir->dirent.len + 2);
	memcpy(path, dir->dirent.path, dir->dirent.len);
	path[dir->dirent.len] = DOS_PATHSEP;
	strcpy(&path[dir->dirent.len + 1], name);
	errnum = smb_proc_create(&dir->server->server, path, strlen(path), &entry);
	free(path);
	invalidate_dircache();
	return errnum;
}

/* ------------------------------------------------------------------------- */

int	smba_mkdir(smba_file_t *dir, const char *name, smba_stat_t *attr)
{
char	*path;
int		errnum;

	if((errnum = make_open(dir, 0)) < 0)
		return errnum;
	path = malloc(strlen(name) + dir->dirent.len + 2);
	memcpy(path, dir->dirent.path, dir->dirent.len);
	path[dir->dirent.len] = DOS_PATHSEP;
	strcpy(&path[dir->dirent.len + 1], name);
	errnum = smb_proc_mkdir(&dir->server->server, path, strlen(path));
	free(path);
	invalidate_dircache();
	return errnum;
}

/* ------------------------------------------------------------------------- */

static void	close_path(smba_server_t *s, char *path)
{
smba_file_t	*p;

	for(p=s->open_files.head;p!=NULL;p=p->next){
		if(p->is_valid && uncase_strcmp(p->dirent.path, path) == 0){
			if(p->dirent.opened)
				smb_proc_close(&s->server, p->dirent.fileid, p->dirent.mtime);
			p->dirent.opened = 0;
			p->is_valid = 0;
		}
	}
}

/* ------------------------------------------------------------------------- */

int	smba_remove(smba_server_t *s, char *path)
{
	close_path(s, path);
	invalidate_dircache();
	return smb_proc_unlink(&s->server, path, strlen(path));
}

/* ------------------------------------------------------------------------- */

int	smba_rmdir(smba_server_t *s, char *path)
{
	close_path(s, path);
	invalidate_dircache();
	return smb_proc_rmdir(&s->server, path, strlen(path));
}

/* ------------------------------------------------------------------------- */

int	smba_rename(smba_server_t *s, char *from, char *to)
{
	close_path(s, from);
	invalidate_dircache();
	smba_remove(s, to);		/* ignore the expected error */
	return smb_proc_mv(&s->server, from, strlen(from), to, strlen(to));
}

/* ------------------------------------------------------------------------- */

int	smba_statfs(smba_server_t *s, long *bsize, long *blocks, long *bfree)
{
struct smb_dskattr	dskattr;
struct super_block	super;
struct smb_sb_info	sb;
int					errnum = 0;

	super.u.generic_sbp = &sb;
	sb.s_server = s->server;
	errnum = smb_proc_dskattr(&super, &dskattr);
	if(errnum)
		return errnum;
	*bsize = dskattr.blocksize * dskattr.allocblocks;
	*blocks = dskattr.total;
	*bfree = dskattr.free;
	return 0;
}

/* ------------------------------------------------------------------------- */

void smb_invalidate_all_inodes(struct smb_server *server)
{
smba_server_t	*p;
smba_file_t		*f;

	invalidate_dircache();
	for(p=servers.head;p!=NULL;p=p->next){
		if(&p->server == server){
			for(f=p->open_files.head;f!=NULL;f=f->next){
				f->dirent.opened = 0;
				f->is_valid = 0;
			}
		}
	}
}

/* ------------------------------------------------------------------------- */

void	smba_regular(void)	/* write back all dirty attributes */
{
smba_server_t	*s;
smba_file_t		*f;

	for(s=servers.head;s!=NULL;s=s->next){
		for(f=s->open_files.head;f!=NULL;f=f->next){
			if(f->attr_dirty)
				write_attr(f);
		}
	}
}

/* ------------------------------------------------------------------------- */

void	smba_init(void)
{
int		i;

	list_init(&servers);
	for(i=0;i<DIRCACHE_SIZE;i++){
		the_dircache.cache[i].path = malloc(SMB_MAXNAMELEN + 1);
	}
}

/* ------------------------------------------------------------------------- */
