/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * File System Virtual Channel
 *
 * Copyright 2010-2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 * Copyright 2010-2011 Vic Lee
 * Copyright 2012 Gerald Richter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifndef _WIN32
#define __USE_LARGEFILE64
#define _LARGEFILE_SOURCE
#define _LARGEFILE64_SOURCE

#include <sys/time.h>
#endif

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>

#include <winpr/crt.h>
#include <winpr/file.h>

#include <winpr/stream.h>
#include <freerdp/channels/rdpdr.h>
#include <freerdp/utils/svc_plugin.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include "drive_file.h"

#ifdef _WIN32
#pragma warning(push)
#pragma warning(disable: 4244)
#endif

static void drive_file_fix_path(char* path)
{
	int i;
	int length;

	length = strlen(path);

	for (i = 0; i < length; i++)
	{
		if (path[i] == '\\')
			path[i] = '/';
	}

#ifdef WIN32
	if ((length == 3) && (path[1] == ':') && (path[2] == '/'))
		return;
#else
	if ((length == 1) && (path[0] == '/'))
		return;
#endif

	if ((length > 0) && (path[length - 1] == '/'))
		path[length - 1] = '\0';
}

static char* drive_file_combine_fullpath(const char* base_path, const char* path)
{
	char* fullpath;

	fullpath = (char*) malloc(strlen(base_path) + strlen(path) + 1);
	strcpy(fullpath, base_path);
	strcat(fullpath, path);
	drive_file_fix_path(fullpath);

	return fullpath;
}

static BOOL drive_file_remove_dir(const char* path)
{
	DIR* dir;
	char* p;
	struct STAT st;
	struct dirent* pdirent;
	BOOL ret = TRUE;

	dir = opendir(path);

	if (dir == NULL)
		return FALSE;

	pdirent = readdir(dir);

	while (pdirent)
	{
		if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)
		{
			pdirent = readdir(dir);
			continue;
		}

		p = (char*) malloc(strlen(path) + strlen(pdirent->d_name) + 2);
		sprintf(p, "%s/%s", path, pdirent->d_name);

		if (STAT(p, &st) != 0)
		{
			DEBUG_WARN("stat %s failed.", p);
			ret = FALSE;
		}
		else if (S_ISDIR(st.st_mode))
		{
			ret = drive_file_remove_dir(p);
		}
		else if (unlink(p) < 0)
		{
			DEBUG_WARN("unlink %s failed.", p);
			ret = FALSE;
		}
		else
		{
			ret = TRUE;
		}
		
		free(p);

		if (!ret)
			break;

		pdirent = readdir(dir);
	}

	closedir(dir);

	if (ret)
	{
		if (rmdir(path) < 0)
		{
			DEBUG_WARN("rmdir %s failed.", path);
			ret = FALSE;
		}
	}

	return ret;
}

static void drive_file_set_fullpath(DRIVE_FILE* file, char* fullpath)
{
	free(file->fullpath);
	file->fullpath = fullpath;
	file->filename = strrchr(file->fullpath, '/');

	if (file->filename == NULL)
		file->filename = file->fullpath;
	else
		file->filename += 1;
}

static BOOL drive_file_init(DRIVE_FILE* file, UINT32 DesiredAccess, UINT32 CreateDisposition, UINT32 CreateOptions)
{
	struct STAT st;
	BOOL exists;
#ifdef WIN32
        const static int mode = _S_IREAD | _S_IWRITE ;
#else
        const static int mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
	BOOL largeFile = FALSE;
#endif
	int oflag = 0;

	if (STAT(file->fullpath, &st) == 0)
	{
		file->is_dir = (S_ISDIR(st.st_mode) ? TRUE : FALSE);
#ifndef WIN32
		if (st.st_size > (unsigned long) 0x07FFFFFFF)
			largeFile = TRUE;
#endif
		exists = TRUE;
	}
	else
	{
		file->is_dir = ((CreateOptions & FILE_DIRECTORY_FILE) ? TRUE : FALSE);
		if (file->is_dir)
		{
			/* Should only create the directory if the disposition allows for it */
			if ((CreateDisposition == FILE_OPEN_IF) || (CreateDisposition == FILE_CREATE))
			{
				if (mkdir(file->fullpath, mode) != 0)
				{
					file->err = errno;
					return TRUE;
				}
			}
		}
		exists = FALSE;
	}

	if (file->is_dir)
	{
		file->dir = opendir(file->fullpath);

		if (file->dir == NULL)
		{
			file->err = errno;
			return TRUE;
		}
	}
	else
	{
		switch (CreateDisposition)
		{
			case FILE_SUPERSEDE:
				oflag = O_TRUNC | O_CREAT;
				break;
			case FILE_OPEN:
				break;
			case FILE_CREATE:
				oflag = O_CREAT | O_EXCL;
				break;
			case FILE_OPEN_IF:
				oflag = O_CREAT;
				break;
			case FILE_OVERWRITE:
				oflag = O_TRUNC;
				break;
			case FILE_OVERWRITE_IF:
				oflag = O_TRUNC | O_CREAT;
				break;
			default:
				break;
		}

		if ((CreateOptions & FILE_DELETE_ON_CLOSE) && (DesiredAccess & DELETE))
		{
			file->delete_pending = TRUE;
		}

		if ((DesiredAccess & GENERIC_ALL)
			|| (DesiredAccess & GENERIC_WRITE)
			|| (DesiredAccess & FILE_WRITE_DATA)
			|| (DesiredAccess & FILE_APPEND_DATA))
		{
			oflag |= O_RDWR;
		}
		else
		{
			oflag |= O_RDONLY;
		}
#ifndef WIN32
		if (largeFile)
		{
		    oflag |= O_LARGEFILE;
		}
#endif
		file->fd = OPEN(file->fullpath, oflag, mode);

		if (file->fd == -1)
		{
			file->err = errno;
			return TRUE;
		}
	}

	return TRUE;
}

DRIVE_FILE* drive_file_new(const char* base_path, const char* path, UINT32 id,
	UINT32 DesiredAccess, UINT32 CreateDisposition, UINT32 CreateOptions)
{
	DRIVE_FILE* file;

	file = (DRIVE_FILE*) malloc(sizeof(DRIVE_FILE));
	ZeroMemory(file, sizeof(DRIVE_FILE));

	file->id = id;
	file->basepath = (char*) base_path;
	drive_file_set_fullpath(file, drive_file_combine_fullpath(base_path, path));
	file->fd = -1;

	if (!drive_file_init(file, DesiredAccess, CreateDisposition, CreateOptions))
	{
		(void) drive_file_free(file, TRUE);
		return NULL;
	}

	return file;
}

int drive_file_free(DRIVE_FILE* file, BOOL recursive)
{
	int retval;

	retval = 0;

	if (file->fd != -1)
		close(file->fd);

	if (file->dir != NULL)
		closedir(file->dir);

	if (file->delete_pending)
	{
		if (file->is_dir)
		{
			if (recursive)
			{
				if (!drive_file_remove_dir(file->fullpath))
				{
					retval = EPERM;
				}
			}
			else
			{
				if (rmdir(file->fullpath) != 0)
				{
					retval = errno;
				}
			}
		}
		else
		{
			if (unlink(file->fullpath) != 0)
			{
				retval = errno;
			}
		}
	}

	free(file->pattern);
	free(file->fullpath);
	free(file);

	return retval;
}

BOOL drive_file_seek(DRIVE_FILE* file, UINT64 Offset)
{
	if (file->is_dir || file->fd == -1)
		return FALSE;

	if (LSEEK(file->fd, Offset, SEEK_SET) == (off_t)-1)
		return FALSE;

	return TRUE;
}

BOOL drive_file_read(DRIVE_FILE* file, BYTE* buffer, UINT32* Length)
{
	size_t left;
	BYTE *bufptr;

	if (file->is_dir || file->fd == -1)
		return FALSE;

	bufptr = buffer;
	left = *Length;
	while (left > 0)
	{
		ssize_t bytesRead;
		bytesRead = read(file->fd, bufptr, left);
		if (bytesRead < 0)
		{
			if ((errno == EAGAIN) || (errno == EINTR))
				continue;
			else
				return FALSE;
		}
		else if (bytesRead == 0) break;		/* EOF */
		bufptr += bytesRead;
		left -= bytesRead;
	}

	*Length -= left;

	return TRUE;
}

BOOL drive_file_write(DRIVE_FILE* file, const BYTE* buffer, UINT32 Length)
{
	if (file->is_dir || file->fd == -1)
		return FALSE;

	while (Length > 0)
	{
		ssize_t bytesWritten;
		bytesWritten = write(file->fd, buffer, Length);

		if (bytesWritten < 0)
		{
			if ((errno == EAGAIN) || (errno == EINTR))
				continue;
			else
				return FALSE;
		}

		Length -= bytesWritten;
		buffer += bytesWritten;
	}

	return TRUE;
}

BOOL drive_file_query_information(DRIVE_FILE* file, UINT32 FsInformationClass, wStream* output)
{
	struct STAT st;

	if (STAT(file->fullpath, &st) != 0)
	{
		Stream_Write_UINT32(output, 0); /* Length */
		return FALSE;
	}

	switch (FsInformationClass)
	{
		case FileBasicInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232094.aspx */
			Stream_Write_UINT32(output, 36); /* Length */
			Stream_EnsureRemainingCapacity(output, 36);
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* CreationTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_atime)); /* LastAccessTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* LastWriteTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_ctime)); /* ChangeTime */
			Stream_Write_UINT32(output, FILE_ATTR_SYSTEM_TO_RDP(file, st)); /* FileAttributes */
			/* Reserved(4), MUST NOT be added! */
			break;

		case FileStandardInformation:
			/*  http://msdn.microsoft.com/en-us/library/cc232088.aspx */
			Stream_Write_UINT32(output, 22); /* Length */
			Stream_EnsureRemainingCapacity(output, 22);
			Stream_Write_UINT64(output, st.st_size); /* AllocationSize */
			Stream_Write_UINT64(output, st.st_size); /* EndOfFile */
			Stream_Write_UINT32(output, st.st_nlink); /* NumberOfLinks */
			Stream_Write_UINT8(output, file->delete_pending ? 1 : 0); /* DeletePending */
			Stream_Write_UINT8(output, file->is_dir ? 1 : 0); /* Directory */
			/* Reserved(2), MUST NOT be added! */
			break;

		case FileAttributeTagInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232093.aspx */
			Stream_Write_UINT32(output, 8); /* Length */
			Stream_EnsureRemainingCapacity(output, 8);
			Stream_Write_UINT32(output, FILE_ATTR_SYSTEM_TO_RDP(file, st)); /* FileAttributes */
			Stream_Write_UINT32(output, 0); /* ReparseTag */
			break;

		default:
			Stream_Write_UINT32(output, 0); /* Length */
			DEBUG_WARN("invalid FsInformationClass %d", FsInformationClass);
			return FALSE;
	}
	return TRUE;
}

BOOL drive_file_set_information(DRIVE_FILE* file, UINT32 FsInformationClass, UINT32 Length, wStream* input)
{
	char* s = NULL;
        mode_t m;
	UINT64 size;
	int status;
	char* fullpath;
	struct STAT st;
	struct timeval tv[2];
	UINT64 LastWriteTime;
	UINT32 FileAttributes;
	UINT32 FileNameLength;

	m = 0;

	switch (FsInformationClass)
	{
		case FileBasicInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232094.aspx */
			Stream_Seek_UINT64(input); /* CreationTime */
			Stream_Seek_UINT64(input); /* LastAccessTime */
			Stream_Read_UINT64(input, LastWriteTime);
			Stream_Seek_UINT64(input); /* ChangeTime */
			Stream_Read_UINT32(input, FileAttributes);

			if (FSTAT(file->fd, &st) != 0)
				return FALSE;

			tv[0].tv_sec = st.st_atime;
			tv[0].tv_usec = 0;
			tv[1].tv_sec = (LastWriteTime > 0 ? FILE_TIME_RDP_TO_SYSTEM(LastWriteTime) : st.st_mtime);
			tv[1].tv_usec = 0;
#ifndef WIN32
/* TODO on win32 */                        
#ifdef ANDROID
			utimes(file->fullpath, tv);
#else
			futimes(file->fd, tv);
#endif

			if (FileAttributes > 0)
			{
				m = st.st_mode;
				if ((FileAttributes & FILE_ATTRIBUTE_READONLY) == 0)
					m |= S_IWUSR;
				else
					m &= ~S_IWUSR;
				if (m != st.st_mode)
					fchmod(file->fd, m);
			}
#endif
                        break;

		case FileEndOfFileInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232067.aspx */
		case FileAllocationInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232076.aspx */
			Stream_Read_UINT64(input, size);
			if (ftruncate(file->fd, size) != 0)
				return FALSE;
			break;

		case FileDispositionInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232098.aspx */
			/* http://msdn.microsoft.com/en-us/library/cc241371.aspx */
			if (Length)
				Stream_Read_UINT8(input, file->delete_pending);
			else
				file->delete_pending = 1;
			if (file->delete_pending && file->is_dir)
			{
				/* mstsc causes this to FAIL if the directory is not empty,
				 * and that's what the server is expecting.  If we wait for
				 * the close to flag a failure, cut and paste of a folder
				 * will lose the folder's contents.
				 */
				int status;
				status = rmdir(file->fullpath);
				if (status == 0)
				{
					/* Put it back so the normal pending delete will work. */
					mkdir(file->fullpath, 0755);
				}
				else
				{
					return FALSE;
				}
			}
			break;

		case FileRenameInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232085.aspx */
			Stream_Seek_UINT8(input); /* ReplaceIfExists */
			Stream_Seek_UINT8(input); /* RootDirectory */
			Stream_Read_UINT32(input, FileNameLength);

			status = ConvertFromUnicode(CP_UTF8, 0, (WCHAR*) Stream_Pointer(input),
					FileNameLength / 2, &s, 0, NULL, NULL);

			if (status < 1)
				s = (char*) calloc(1, 1);

			fullpath = drive_file_combine_fullpath(file->basepath, s);
			free(s);

			/* TODO rename does not work on win32 */
                        if (rename(file->fullpath, fullpath) == 0)
			{
				DEBUG_SVC("renamed %s to %s", file->fullpath, fullpath);
				drive_file_set_fullpath(file, fullpath);
			}
			else
			{
				DEBUG_WARN("rename %s to %s failed, errno = %d", file->fullpath, fullpath, errno);
				free(fullpath);
				return FALSE;
			}

			break;

		default:
			DEBUG_WARN("invalid FsInformationClass %d", FsInformationClass);
			return FALSE;
	}

	return TRUE;
}

BOOL drive_file_query_directory(DRIVE_FILE* file, UINT32 FsInformationClass, BYTE InitialQuery,
	const char* path, wStream* output)
{
	int length;
	BOOL ret;
	WCHAR* ent_path;
	struct STAT st;
	struct dirent* ent;

	DEBUG_SVC("path %s FsInformationClass %d InitialQuery %d", path, FsInformationClass, InitialQuery);

	if (!file->dir)
	{
		Stream_Write_UINT32(output, 0); /* Length */
		Stream_Write_UINT8(output, 0); /* Padding */
		return FALSE;
	}

	if (InitialQuery != 0)
	{
		rewinddir(file->dir);
		free(file->pattern);

		if (path[0])
			file->pattern = _strdup(strrchr(path, '\\') + 1);
		else
			file->pattern = NULL;
	}

	if (file->pattern)
	{
		do
		{
			ent = readdir(file->dir);

			if (ent == NULL)
				continue;

			if (FilePatternMatchA(ent->d_name, file->pattern))
				break;

		}
		while (ent);
	}
	else
	{
		ent = readdir(file->dir);
	}

	if (ent == NULL)
	{
		DEBUG_SVC("  pattern %s not found.", file->pattern);
		Stream_Write_UINT32(output, 0); /* Length */
		Stream_Write_UINT8(output, 0); /* Padding */
		return FALSE;
	}

	memset(&st, 0, sizeof(struct STAT));
	ent_path = (WCHAR*) malloc(strlen(file->fullpath) + strlen(ent->d_name) + 2);
	sprintf((char*) ent_path, "%s/%s", file->fullpath, ent->d_name);

	if (STAT((char*) ent_path, &st) != 0)
	{
		DEBUG_WARN("stat %s failed. errno = %d", (char*) ent_path, errno);
	}

	DEBUG_SVC("  pattern %s matched %s", file->pattern, ent_path);
	free(ent_path);
	ent_path = NULL;

	length = ConvertToUnicode(CP_UTF8, 0, ent->d_name, -1, &ent_path, 0) * 2;

	ret = TRUE;

	switch (FsInformationClass)
	{
		case FileDirectoryInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232097.aspx */
			Stream_Write_UINT32(output, 64 + length); /* Length */
			Stream_EnsureRemainingCapacity(output, 64 + length);
			Stream_Write_UINT32(output, 0); /* NextEntryOffset */
			Stream_Write_UINT32(output, 0); /* FileIndex */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* CreationTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_atime)); /* LastAccessTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* LastWriteTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_ctime)); /* ChangeTime */
			Stream_Write_UINT64(output, st.st_size); /* EndOfFile */
			Stream_Write_UINT64(output, st.st_size); /* AllocationSize */
			Stream_Write_UINT32(output, FILE_ATTR_SYSTEM_TO_RDP(file, st)); /* FileAttributes */
			Stream_Write_UINT32(output, length); /* FileNameLength */
			Stream_Write(output, ent_path, length);
			break;

		case FileFullDirectoryInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232068.aspx */
			Stream_Write_UINT32(output, 68 + length); /* Length */
			Stream_EnsureRemainingCapacity(output, 68 + length);
			Stream_Write_UINT32(output, 0); /* NextEntryOffset */
			Stream_Write_UINT32(output, 0); /* FileIndex */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* CreationTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_atime)); /* LastAccessTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* LastWriteTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_ctime)); /* ChangeTime */
			Stream_Write_UINT64(output, st.st_size); /* EndOfFile */
			Stream_Write_UINT64(output, st.st_size); /* AllocationSize */
			Stream_Write_UINT32(output, FILE_ATTR_SYSTEM_TO_RDP(file, st)); /* FileAttributes */
			Stream_Write_UINT32(output, length); /* FileNameLength */
			Stream_Write_UINT32(output, 0); /* EaSize */
			Stream_Write(output, ent_path, length);
			break;

		case FileBothDirectoryInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232095.aspx */
			Stream_Write_UINT32(output, 93 + length); /* Length */
			Stream_EnsureRemainingCapacity(output, 93 + length);
			Stream_Write_UINT32(output, 0); /* NextEntryOffset */
			Stream_Write_UINT32(output, 0); /* FileIndex */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* CreationTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_atime)); /* LastAccessTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_mtime)); /* LastWriteTime */
			Stream_Write_UINT64(output, FILE_TIME_SYSTEM_TO_RDP(st.st_ctime)); /* ChangeTime */
			Stream_Write_UINT64(output, st.st_size); /* EndOfFile */
			Stream_Write_UINT64(output, st.st_size); /* AllocationSize */
			Stream_Write_UINT32(output, FILE_ATTR_SYSTEM_TO_RDP(file, st)); /* FileAttributes */
			Stream_Write_UINT32(output, length); /* FileNameLength */
			Stream_Write_UINT32(output, 0); /* EaSize */
			Stream_Write_UINT8(output, 0); /* ShortNameLength */
			/* Reserved(1), MUST NOT be added! */
			Stream_Zero(output, 24); /* ShortName */
			Stream_Write(output, ent_path, length);
			break;

		case FileNamesInformation:
			/* http://msdn.microsoft.com/en-us/library/cc232077.aspx */
			Stream_Write_UINT32(output, 12 + length); /* Length */
			Stream_EnsureRemainingCapacity(output, 12 + length);
			Stream_Write_UINT32(output, 0); /* NextEntryOffset */
			Stream_Write_UINT32(output, 0); /* FileIndex */
			Stream_Write_UINT32(output, length); /* FileNameLength */
			Stream_Write(output, ent_path, length);
			break;

		default:
			Stream_Write_UINT32(output, 0); /* Length */
			Stream_Write_UINT8(output, 0); /* Padding */
			DEBUG_WARN("invalid FsInformationClass %d", FsInformationClass);
			ret = FALSE;
			break;
	}

	free(ent_path);

	return ret;
}

#ifdef _WIN32
#pragma warning(pop)
#endif
