/**
 * 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
 * Copyright 2015 Thincast Technologies GmbH
 * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
 * Copyright 2016 Inuvika Inc.
 * Copyright 2016 David PHAM-VAN <d.phamvan@inuvika.com>
 * Copyright 2017 Armin Novak <armin.novak@thincast.com>
 * Copyright 2017 Thincast Technologies GmbH
 *
 * 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.
 */

#include <freerdp/config.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <winpr/wtypes.h>
#include <winpr/crt.h>
#include <winpr/string.h>
#include <winpr/path.h>
#include <winpr/file.h>
#include <winpr/stream.h>

#include <freerdp/channels/rdpdr.h>

#include "drive_file.h"

#ifdef WITH_DEBUG_RDPDR
#define DEBUG_WSTR(msg, wstr)                                    \
	do                                                           \
	{                                                            \
		char lpstr[1024] = WINPR_C_ARRAY_INIT;                   \
		(void)ConvertWCharToUtf8(wstr, lpstr, ARRAYSIZE(lpstr)); \
		WLog_DBG(TAG, msg, lpstr);                               \
	} while (0)
#else
#define DEBUG_WSTR(msg, wstr) \
	do                        \
	{                         \
	} while (0)
#endif

static BOOL drive_file_fix_path(WCHAR* path, size_t length)
{
	if ((length == 0) || (length > UINT32_MAX))
		return FALSE;

	WINPR_ASSERT(path);

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

#ifdef WIN32

	if ((length == 3) && (path[1] == L':') && (path[2] == L'/'))
		return FALSE;

#else

	if ((length == 1) && (path[0] == L'/'))
		return FALSE;

#endif

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

	return TRUE;
}

static BOOL contains_dotdot(const WCHAR* path, size_t base_length, size_t path_length)
{
	WCHAR dotdotbuffer[6] = WINPR_C_ARRAY_INIT;
	const WCHAR* dotdot = InitializeConstWCharFromUtf8("..", dotdotbuffer, ARRAYSIZE(dotdotbuffer));
	const WCHAR* tst = path;

	if (path_length < 2)
		return FALSE;

	do
	{
		tst = _wcsstr(tst, dotdot);
		if (!tst)
			return FALSE;

		/* Filter .. sequences in file or directory names */
		if ((base_length == 0) || (*(tst - 1) == L'/') || (*(tst - 1) == L'\\'))
		{
			if (tst + 2 < path + path_length)
			{
				if ((tst[2] == '/') || (tst[2] == '\\') || (tst[2] == '\0'))
					return TRUE;
			}
			else
				return TRUE;
		}
		tst += 2;
	} while (TRUE);

	return FALSE;
}

static WCHAR* drive_file_combine_fullpath(const WCHAR* base_path, const WCHAR* path,
                                          size_t PathWCharLength)
{
	BOOL ok = FALSE;
	WCHAR* fullpath = nullptr;

	if (!base_path || (!path && (PathWCharLength > 0)))
		goto fail;

	{
		const size_t base_path_length = _wcsnlen(base_path, MAX_PATH);
		const size_t length = base_path_length + PathWCharLength + 1;
		fullpath = (WCHAR*)calloc(length, sizeof(WCHAR));

		if (!fullpath)
			goto fail;

		CopyMemory(fullpath, base_path, base_path_length * sizeof(WCHAR));
		if (path)
			CopyMemory(&fullpath[base_path_length], path, PathWCharLength * sizeof(WCHAR));

		if (!drive_file_fix_path(fullpath, length))
			goto fail;

		/* Ensure the path does not contain sequences like '..' */
		if (contains_dotdot(&fullpath[base_path_length], base_path_length, PathWCharLength))
		{
			char* abuffer = ConvertWCharToUtf8Alloc(&fullpath[base_path_length], nullptr);
			WLog_WARN(TAG, "[rdpdr] received invalid file path '%s' from server, aborting!",
			          abuffer);
			free(abuffer);
			goto fail;
		}
	}

	ok = TRUE;
fail:
	if (!ok)
	{
		free(fullpath);
		fullpath = nullptr;
	}
	return fullpath;
}

static BOOL drive_file_set_fullpath(DRIVE_FILE* file, const WCHAR* fullpath)
{
	if (!file || !fullpath)
		return FALSE;

	const size_t len = _wcslen(fullpath);
	free(file->fullpath);
	file->fullpath = nullptr;

	if (len == 0)
		return TRUE;

	file->fullpath = _wcsdup(fullpath);
	if (!file->fullpath)
		return FALSE;

	const WCHAR sep[] = { PathGetSeparatorW(PATH_STYLE_NATIVE), '\0' };
	WCHAR* filename = _wcsrchr(file->fullpath, *sep);
	if (filename && _wcsncmp(filename, sep, ARRAYSIZE(sep)) == 0)
		*filename = '\0';

	return TRUE;
}

static BOOL drive_file_init(DRIVE_FILE* file)
{
	UINT CreateDisposition = 0;
	DWORD dwAttr = GetFileAttributesW(file->fullpath);

	if (dwAttr != INVALID_FILE_ATTRIBUTES)
	{
		/* The file exists */
		file->is_dir = (dwAttr & FILE_ATTRIBUTE_DIRECTORY) != 0;

		if (file->is_dir)
		{
			if (file->CreateDisposition == FILE_CREATE)
			{
				SetLastError(ERROR_ALREADY_EXISTS);
				return FALSE;
			}

			if (file->CreateOptions & FILE_NON_DIRECTORY_FILE)
			{
				SetLastError(ERROR_ACCESS_DENIED);
				return FALSE;
			}

			return TRUE;
		}
		else
		{
			if (file->CreateOptions & FILE_DIRECTORY_FILE)
			{
				SetLastError(ERROR_DIRECTORY);
				return FALSE;
			}
		}
	}
	else
	{
		file->is_dir = ((file->CreateOptions & FILE_DIRECTORY_FILE) != 0);

		if (file->is_dir)
		{
			/* Should only create the directory if the disposition allows for it */
			if ((file->CreateDisposition == FILE_OPEN_IF) ||
			    (file->CreateDisposition == FILE_CREATE))
			{
				if (CreateDirectoryW(file->fullpath, nullptr) != 0)
				{
					return TRUE;
				}
			}

			SetLastError(ERROR_FILE_NOT_FOUND);
			return FALSE;
		}
	}

	if (file->file_handle == INVALID_HANDLE_VALUE)
	{
		switch (file->CreateDisposition)
		{
			case FILE_SUPERSEDE: /* If the file already exists, replace it with the given file. If
			                        it does not, create the given file. */
				CreateDisposition = CREATE_ALWAYS;
				break;

			case FILE_OPEN: /* If the file already exists, open it instead of creating a new file.
			                   If it does not, fail the request and do not create a new file. */
				CreateDisposition = OPEN_EXISTING;
				break;

			case FILE_CREATE: /* If the file already exists, fail the request and do not create or
			                     open the given file. If it does not, create the given file. */
				CreateDisposition = CREATE_NEW;
				break;

			case FILE_OPEN_IF: /* If the file already exists, open it. If it does not, create the
			                      given file. */
				CreateDisposition = OPEN_ALWAYS;
				break;

			case FILE_OVERWRITE: /* If the file already exists, open it and overwrite it. If it does
			                        not, fail the request. */
				CreateDisposition = TRUNCATE_EXISTING;
				break;

			case FILE_OVERWRITE_IF: /* If the file already exists, open it and overwrite it. If it
			                           does not, create the given file. */
				CreateDisposition = CREATE_ALWAYS;
				break;

			default:
				break;
		}

#ifndef WIN32
		file->SharedAccess = 0;
#endif
		file->file_handle = CreateFileW(file->fullpath, file->DesiredAccess, file->SharedAccess,
		                                nullptr, CreateDisposition, file->FileAttributes, nullptr);
	}

#ifdef WIN32
	if (file->file_handle == INVALID_HANDLE_VALUE)
	{
		/* Get the error message, if any. */
		DWORD errorMessageID = GetLastError();

		if (errorMessageID != 0)
		{
			LPSTR messageBuffer = nullptr;
			size_t size =
			    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
			                       FORMAT_MESSAGE_IGNORE_INSERTS,
			                   nullptr, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
			                   (LPSTR)&messageBuffer, 0, nullptr);
			char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
			(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
			WLog_ERR(TAG, "Error in drive_file_init: %s %s", messageBuffer, fullpath);
			/* Free the buffer. */
			LocalFree(messageBuffer);
			/* restore original error code */
			SetLastError(errorMessageID);
		}
	}
#endif

	return file->file_handle != INVALID_HANDLE_VALUE;
}

DRIVE_FILE* drive_file_new(const WCHAR* base_path, const WCHAR* path, UINT32 PathWCharLength,
                           UINT32 id, UINT32 DesiredAccess, UINT32 CreateDisposition,
                           UINT32 CreateOptions, UINT32 FileAttributes, UINT32 SharedAccess)
{
	if (!base_path || (!path && (PathWCharLength > 0)))
		return nullptr;

	DRIVE_FILE* file = (DRIVE_FILE*)calloc(1, sizeof(DRIVE_FILE));

	if (!file)
	{
		WLog_ERR(TAG, "calloc failed!");
		return nullptr;
	}

	file->file_handle = INVALID_HANDLE_VALUE;
	file->find_handle = INVALID_HANDLE_VALUE;
	file->id = id;
	file->basepath = base_path;
	file->FileAttributes = FileAttributes;
	file->DesiredAccess = DesiredAccess;
	file->CreateDisposition = CreateDisposition;
	file->CreateOptions = CreateOptions;
	file->SharedAccess = SharedAccess;

	WCHAR* p = drive_file_combine_fullpath(base_path, path, PathWCharLength);
	(void)drive_file_set_fullpath(file, p);
	free(p);

	if (!drive_file_init(file))
	{
		DWORD lastError = GetLastError();
		drive_file_free(file);
		SetLastError(lastError);
		return nullptr;
	}

	return file;
}

BOOL drive_file_free(DRIVE_FILE* file)
{
	BOOL rc = FALSE;

	if (!file)
		return FALSE;

	if (file->file_handle != INVALID_HANDLE_VALUE)
	{
		(void)CloseHandle(file->file_handle);
		file->file_handle = INVALID_HANDLE_VALUE;
	}

	if (file->find_handle != INVALID_HANDLE_VALUE)
	{
		FindClose(file->find_handle);
		file->find_handle = INVALID_HANDLE_VALUE;
	}

	if (file->CreateOptions & FILE_DELETE_ON_CLOSE)
		file->delete_pending = TRUE;

	if (file->delete_pending)
	{
		if (file->is_dir)
		{
			if (!winpr_RemoveDirectory_RecursiveW(file->fullpath))
				goto fail;
		}
		else if (!DeleteFileW(file->fullpath))
			goto fail;
	}

	rc = TRUE;
fail:
	DEBUG_WSTR("Free %s", file->fullpath);
	free(file->fullpath);
	free(file);
	return rc;
}

BOOL drive_file_seek(DRIVE_FILE* file, UINT64 Offset)
{
	LARGE_INTEGER loffset = WINPR_C_ARRAY_INIT;

	if (!file)
		return FALSE;

	if (Offset > INT64_MAX)
		return FALSE;

	loffset.QuadPart = (LONGLONG)Offset;
	return SetFilePointerEx(file->file_handle, loffset, nullptr, FILE_BEGIN);
}

BOOL drive_file_read(DRIVE_FILE* file, BYTE* buffer, UINT32* Length)
{
	DWORD read = 0;

	if (!file || !buffer || !Length)
		return FALSE;

	DEBUG_WSTR("Read file %s", file->fullpath);

	if (ReadFile(file->file_handle, buffer, *Length, &read, nullptr))
	{
		*Length = read;
		return TRUE;
	}

	return FALSE;
}

BOOL drive_file_write(DRIVE_FILE* file, const BYTE* buffer, UINT32 Length)
{
	DWORD written = 0;

	if (!file || !buffer)
		return FALSE;

	DEBUG_WSTR("Write file %s", file->fullpath);

	while (Length > 0)
	{
		if (!WriteFile(file->file_handle, buffer, Length, &written, nullptr))
			return FALSE;

		Length -= written;
		buffer += written;
	}

	return TRUE;
}

static BOOL drive_file_query_from_handle_information(const DRIVE_FILE* file,
                                                     const BY_HANDLE_FILE_INFORMATION* info,
                                                     UINT32 FsInformationClass, wStream* output)
{
	switch (FsInformationClass)
	{
		case FileBasicInformation:

			/* http://msdn.microsoft.com/en-us/library/cc232094.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 36))
				return FALSE;

			Stream_Write_UINT32(output, 36);                                    /* Length */
			Stream_Write_UINT32(output, info->ftCreationTime.dwLowDateTime);    /* CreationTime */
			Stream_Write_UINT32(output, info->ftCreationTime.dwHighDateTime);   /* CreationTime */
			Stream_Write_UINT32(output, info->ftLastAccessTime.dwLowDateTime);  /* LastAccessTime */
			Stream_Write_UINT32(output, info->ftLastAccessTime.dwHighDateTime); /* LastAccessTime */
			Stream_Write_UINT32(output, info->ftLastWriteTime.dwLowDateTime);   /* LastWriteTime */
			Stream_Write_UINT32(output, info->ftLastWriteTime.dwHighDateTime);  /* LastWriteTime */
			Stream_Write_UINT32(output, info->ftLastWriteTime.dwLowDateTime);   /* ChangeTime */
			Stream_Write_UINT32(output, info->ftLastWriteTime.dwHighDateTime);  /* ChangeTime */
			Stream_Write_UINT32(output, info->dwFileAttributes);                /* FileAttributes */
			/* Reserved(4), MUST NOT be added! */
			break;

		case FileStandardInformation:

			/*  http://msdn.microsoft.com/en-us/library/cc232088.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 22))
				return FALSE;

			Stream_Write_UINT32(output, 22);                          /* Length */
			Stream_Write_UINT32(output, info->nFileSizeLow);          /* AllocationSize */
			Stream_Write_UINT32(output, info->nFileSizeHigh);         /* AllocationSize */
			Stream_Write_UINT32(output, info->nFileSizeLow);          /* EndOfFile */
			Stream_Write_UINT32(output, info->nFileSizeHigh);         /* EndOfFile */
			Stream_Write_UINT32(output, info->nNumberOfLinks);        /* NumberOfLinks */
			Stream_Write_UINT8(output, file->delete_pending ? 1 : 0); /* DeletePending */
			Stream_Write_UINT8(output, (info->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) !=
			                               0); /* Directory */
			/* Reserved(2), MUST NOT be added! */
			break;

		case FileAttributeTagInformation:

			/* http://msdn.microsoft.com/en-us/library/cc232093.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 8))
				return FALSE;

			Stream_Write_UINT32(output, 8);                      /* Length */
			Stream_Write_UINT32(output, info->dwFileAttributes); /* FileAttributes */
			Stream_Write_UINT32(output, 0);                      /* ReparseTag */
			break;

		default:
			/* Unhandled FsInformationClass */
			WLog_WARN(TAG, "Unhandled FSInformationClass %s [0x%08" PRIx32 "]",
			          FSInformationClass2Tag(FsInformationClass), FsInformationClass);
			return FALSE;
	}

	return TRUE;
}

static BOOL drive_file_query_from_attributes(const DRIVE_FILE* file,
                                             const WIN32_FILE_ATTRIBUTE_DATA* attrib,
                                             UINT32 FsInformationClass, wStream* output)
{
	switch (FsInformationClass)
	{
		case FileBasicInformation:

			/* http://msdn.microsoft.com/en-us/library/cc232094.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 36))
				return FALSE;

			Stream_Write_UINT32(output, 36);                                    /* Length */
			Stream_Write_UINT32(output, attrib->ftCreationTime.dwLowDateTime);  /* CreationTime */
			Stream_Write_UINT32(output, attrib->ftCreationTime.dwHighDateTime); /* CreationTime */
			Stream_Write_UINT32(output,
			                    attrib->ftLastAccessTime.dwLowDateTime); /* LastAccessTime */
			Stream_Write_UINT32(output,
			                    attrib->ftLastAccessTime.dwHighDateTime);       /* LastAccessTime */
			Stream_Write_UINT32(output, attrib->ftLastWriteTime.dwLowDateTime); /* LastWriteTime */
			Stream_Write_UINT32(output, attrib->ftLastWriteTime.dwHighDateTime); /* LastWriteTime */
			Stream_Write_UINT32(output, attrib->ftLastWriteTime.dwLowDateTime);  /* ChangeTime */
			Stream_Write_UINT32(output, attrib->ftLastWriteTime.dwHighDateTime); /* ChangeTime */
			Stream_Write_UINT32(output, attrib->dwFileAttributes); /* FileAttributes */
			/* Reserved(4), MUST NOT be added! */
			break;

		case FileStandardInformation:

			/*  http://msdn.microsoft.com/en-us/library/cc232088.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 22))
				return FALSE;

			Stream_Write_UINT32(output, 22);                          /* Length */
			Stream_Write_UINT32(output, attrib->nFileSizeLow);        /* AllocationSize */
			Stream_Write_UINT32(output, attrib->nFileSizeHigh);       /* AllocationSize */
			Stream_Write_UINT32(output, attrib->nFileSizeLow);        /* EndOfFile */
			Stream_Write_UINT32(output, attrib->nFileSizeHigh);       /* EndOfFile */
			Stream_Write_UINT32(output, 0);                           /* NumberOfLinks */
			Stream_Write_UINT8(output, file->delete_pending ? 1 : 0); /* DeletePending */
			Stream_Write_UINT8(output, (attrib->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) !=
			                               0); /* Directory */
			/* Reserved(2), MUST NOT be added! */
			break;

		case FileAttributeTagInformation:

			/* http://msdn.microsoft.com/en-us/library/cc232093.aspx */
			if (!Stream_EnsureRemainingCapacity(output, 4 + 8))
				return FALSE;

			Stream_Write_UINT32(output, 8);                        /* Length */
			Stream_Write_UINT32(output, attrib->dwFileAttributes); /* FileAttributes */
			Stream_Write_UINT32(output, 0);                        /* ReparseTag */
			break;

		default:
			/* Unhandled FsInformationClass */
			WLog_WARN(TAG, "Unhandled FSInformationClass %s [0x%08" PRIx32 "]",
			          FSInformationClass2Tag(FsInformationClass), FsInformationClass);
			return FALSE;
	}

	return TRUE;
}

BOOL drive_file_query_information(DRIVE_FILE* file, UINT32 FsInformationClass, wStream* output)
{
	BY_HANDLE_FILE_INFORMATION fileInformation = WINPR_C_ARRAY_INIT;
	BOOL status = 0;

	if (!file || !output)
		return FALSE;

	if ((file->file_handle != INVALID_HANDLE_VALUE) &&
	    GetFileInformationByHandle(file->file_handle, &fileInformation))
		return drive_file_query_from_handle_information(file, &fileInformation, FsInformationClass,
		                                                output);

	if (!file->is_dir)
	{
		HANDLE hFile = CreateFileW(file->fullpath, 0, FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
		                           FILE_ATTRIBUTE_NORMAL, nullptr);
		if (hFile != INVALID_HANDLE_VALUE)
		{
			status = GetFileInformationByHandle(hFile, &fileInformation);
			(void)CloseHandle(hFile);
			if (!status)
				goto out_fail;

			if (!drive_file_query_from_handle_information(file, &fileInformation,
			                                              FsInformationClass, output))
				goto out_fail;

			return TRUE;
		}
	}

	/* If we failed before (i.e. if information for a drive is queried) fall back to
	 * GetFileAttributesExW */
	{
		WIN32_FILE_ATTRIBUTE_DATA fileAttributes = WINPR_C_ARRAY_INIT;
		if (!GetFileAttributesExW(file->fullpath, GetFileExInfoStandard, &fileAttributes))
			goto out_fail;

		if (!drive_file_query_from_attributes(file, &fileAttributes, FsInformationClass, output))
			goto out_fail;
	}

	return TRUE;
out_fail:
	Stream_Write_UINT32(output, 0); /* Length */
	return FALSE;
}

static BOOL drive_file_set_basic_information(DRIVE_FILE* file, UINT32 Length, wStream* input)
{
	WINPR_ASSERT(file);

	const uint32_t expect = 36;
	if (Length != expect)
	{
		WLog_WARN(TAG, "Unexpected Length=%" PRIu32 ", expected %" PRIu32, Length, expect);
		return FALSE;
	}

	/* http://msdn.microsoft.com/en-us/library/cc232094.aspx */
	const ULARGE_INTEGER liCreationTime = { .QuadPart = Stream_Get_UINT64(input) };
	const ULARGE_INTEGER liLastAccessTime = { .QuadPart = Stream_Get_UINT64(input) };
	const ULARGE_INTEGER liLastWriteTime = { .QuadPart = Stream_Get_UINT64(input) };
	const ULARGE_INTEGER liChangeTime = { .QuadPart = Stream_Get_UINT64(input) };
	const uint32_t FileAttributes = Stream_Get_UINT32(input);

	if (!PathFileExistsW(file->fullpath))
		return FALSE;

	if (file->file_handle == INVALID_HANDLE_VALUE)
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath) - 1);

		WLog_ERR(TAG, "Unable to set file time %s (%" PRIu32 ")", fullpath, GetLastError());
		return FALSE;
	}

	FILETIME ftCreationTime = WINPR_C_ARRAY_INIT;
	FILETIME ftLastAccessTime = WINPR_C_ARRAY_INIT;
	FILETIME ftLastWriteTime = WINPR_C_ARRAY_INIT;
	FILETIME* pftCreationTime = nullptr;
	FILETIME* pftLastAccessTime = nullptr;
	FILETIME* pftLastWriteTime = nullptr;
	if (liCreationTime.QuadPart != 0)
	{
		ftCreationTime.dwHighDateTime = liCreationTime.u.HighPart;
		ftCreationTime.dwLowDateTime = liCreationTime.u.LowPart;
		pftCreationTime = &ftCreationTime;
	}

	if (liLastAccessTime.QuadPart != 0)
	{
		ftLastAccessTime.dwHighDateTime = liLastAccessTime.u.HighPart;
		ftLastAccessTime.dwLowDateTime = liLastAccessTime.u.LowPart;
		pftLastAccessTime = &ftLastAccessTime;
	}

	if (liLastWriteTime.QuadPart != 0)
	{
		ftLastWriteTime.dwHighDateTime = liLastWriteTime.u.HighPart;
		ftLastWriteTime.dwLowDateTime = liLastWriteTime.u.LowPart;
		pftLastWriteTime = &ftLastWriteTime;
	}

	if (liChangeTime.QuadPart != 0 && liChangeTime.QuadPart > liLastWriteTime.QuadPart)
	{
		ftLastWriteTime.dwHighDateTime = liChangeTime.u.HighPart;
		ftLastWriteTime.dwLowDateTime = liChangeTime.u.LowPart;
		pftLastWriteTime = &ftLastWriteTime;
	}

	DEBUG_WSTR("SetFileTime %s", file->fullpath);

	if (!SetFileAttributesW(file->fullpath, FileAttributes))
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
		WLog_ERR(TAG, "Unable to set file attributes for %s", fullpath);
		return FALSE;
	}

	if (!SetFileTime(file->file_handle, pftCreationTime, pftLastAccessTime, pftLastWriteTime))
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
		WLog_ERR(TAG, "Unable to set file time for %s", fullpath);
		return FALSE;
	}
	return TRUE;
}

static BOOL drive_file_set_alloc_information(DRIVE_FILE* file, UINT32 Length, wStream* input)
{
	WINPR_ASSERT(file);
	const uint32_t expect = 8;
	if (Length != expect)
	{
		WLog_WARN(TAG, "Unexpected Length=%" PRIu32 ", expected %" PRIu32, Length, expect);
		return FALSE;
	}

	/* http://msdn.microsoft.com/en-us/library/cc232076.aspx */
	const int64_t size = Stream_Get_INT64(input);

	if (file->file_handle == INVALID_HANDLE_VALUE)
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
		WLog_ERR(TAG, "Unable to truncate %s to %" PRId64 " (%" PRIu32 ")", fullpath, size,
		         GetLastError());
		return FALSE;
	}

	LARGE_INTEGER liSize = { .QuadPart = size };

	if (!SetFilePointerEx(file->file_handle, liSize, nullptr, FILE_BEGIN))
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
		WLog_ERR(TAG, "Unable to truncate %s to %" PRId64 " (%" PRIu32 ")", fullpath, size,
		         GetLastError());
		return FALSE;
	}

	DEBUG_WSTR("Truncate %s", file->fullpath);

	if (SetEndOfFile(file->file_handle) == 0)
	{
		char fullpath[MAX_PATH] = WINPR_C_ARRAY_INIT;
		(void)ConvertWCharToUtf8(file->fullpath, fullpath, sizeof(fullpath));
		WLog_ERR(TAG, "Unable to truncate %s to %" PRId64 " (%" PRIu32 ")", fullpath, size,
		         GetLastError());
		return FALSE;
	}

	return TRUE;
}

static BOOL drive_file_set_disposition_information(DRIVE_FILE* file, UINT32 Length, wStream* input)
{
	WINPR_ASSERT(file);
	uint8_t delete_pending = 0;
	/* http://msdn.microsoft.com/en-us/library/cc232098.aspx */
	/* http://msdn.microsoft.com/en-us/library/cc241371.aspx */
	if (file->is_dir && !PathIsDirectoryEmptyW(file->fullpath))
	{
		SetLastError(ERROR_DIR_NOT_EMPTY);
		return FALSE;
	}

	if (Length)
	{
		const uint32_t expect = 1;
		if (Length != expect)
			WLog_DBG(TAG, "Unexpected Length=%" PRIu32 ", expected %" PRIu32, Length, expect);

		delete_pending = Stream_Get_UINT8(input);
	}
	else
		delete_pending = 1;

	if (delete_pending)
	{
		DEBUG_WSTR("SetDeletePending %s", file->fullpath);
		const uint32_t attr = GetFileAttributesW(file->fullpath);

		if (attr & FILE_ATTRIBUTE_READONLY)
		{
			SetLastError(ERROR_ACCESS_DENIED);
			return FALSE;
		}
	}

	file->delete_pending = delete_pending;
	return TRUE;
}

static BOOL drive_file_set_rename_information(DRIVE_FILE* file, UINT32 Length, wStream* input)
{
	WINPR_ASSERT(file);

	const uint32_t expect = 6;
	if (Length < expect)
	{
		WLog_WARN(TAG, "Unexpected Length=%" PRIu32 ", expected at least %" PRIu32, Length, expect);
		return FALSE;
	}

	/* http://msdn.microsoft.com/en-us/library/cc232085.aspx */
	const uint8_t ReplaceIfExists = Stream_Get_UINT8(input);
	Stream_Seek_UINT8(input); /* RootDirectory */
	const uint64_t FileNameLength = Stream_Get_UINT32(input);

	if (Length != expect + FileNameLength)
	{
		WLog_WARN(TAG, "Unexpected Length=%" PRIu32 ", expected %" PRIu64, Length,
		          expect + FileNameLength);
		return FALSE;
	}

	WCHAR* fullpath = drive_file_combine_fullpath(file->basepath, Stream_ConstPointer(input),
	                                              FileNameLength / sizeof(WCHAR));

	if (!fullpath)
		return FALSE;

#ifdef _WIN32

	if (file->file_handle != INVALID_HANDLE_VALUE)
	{
		(void)CloseHandle(file->file_handle);
		file->file_handle = INVALID_HANDLE_VALUE;
	}

#endif
	DEBUG_WSTR("MoveFileExW %s", file->fullpath);

	if (MoveFileExW(file->fullpath, fullpath,
	                MOVEFILE_COPY_ALLOWED | (ReplaceIfExists ? MOVEFILE_REPLACE_EXISTING : 0)))
	{
		const BOOL rc = drive_file_set_fullpath(file, fullpath);
		free(fullpath);
		if (!rc)
			return FALSE;
	}
	else
	{
		free(fullpath);
		return FALSE;
	}

#ifdef _WIN32
	drive_file_init(file);
#endif
	return TRUE;
}

BOOL drive_file_set_information(DRIVE_FILE* file, UINT32 FsInformationClass, UINT32 Length,
                                wStream* input)
{
	if (!file || !input)
		return FALSE;

	if (!Stream_CheckAndLogRequiredLength(TAG, input, Length))
		return FALSE;

	switch (FsInformationClass)
	{
		case FileBasicInformation:
			return drive_file_set_basic_information(file, Length, input);

		case FileEndOfFileInformation:
		/* http://msdn.microsoft.com/en-us/library/cc232067.aspx */
		case FileAllocationInformation:
			return drive_file_set_alloc_information(file, Length, input);

		case FileDispositionInformation:
			return drive_file_set_disposition_information(file, Length, input);

		case FileRenameInformation:
			return drive_file_set_rename_information(file, Length, input);

		default:
			WLog_WARN(TAG, "Unhandled FSInformationClass %s [0x%08" PRIx32 "]",
			          FSInformationClass2Tag(FsInformationClass), FsInformationClass);
			return FALSE;
	}

	return TRUE;
}

static BOOL drive_file_query_dir_info(DRIVE_FILE* file, wStream* output, size_t length)
{
	WINPR_ASSERT(file);
	WINPR_ASSERT(output);

	/* http://msdn.microsoft.com/en-us/library/cc232097.aspx */
	if (!Stream_EnsureRemainingCapacity(output, 4 + 64 + length))
		return FALSE;

	if (length > UINT32_MAX - 64)
		return FALSE;

	Stream_Write_UINT32(output, (UINT32)(64 + length));                        /* Length */
	Stream_Write_UINT32(output, 0);                                            /* NextEntryOffset */
	Stream_Write_UINT32(output, 0);                                            /* FileIndex */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwHighDateTime);       /* LastAccessTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime);  /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);                   /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);                  /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);     /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);    /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */
	Stream_Write_UINT32(output, (UINT32)length);                   /* FileNameLength */
	Stream_Write(output, file->find_data.cFileName, length);
	return TRUE;
}

static BOOL drive_file_query_full_dir_info(DRIVE_FILE* file, wStream* output, size_t length)
{
	WINPR_ASSERT(file);
	WINPR_ASSERT(output);
	/* http://msdn.microsoft.com/en-us/library/cc232068.aspx */
	if (!Stream_EnsureRemainingCapacity(output, 4 + 68 + length))
		return FALSE;

	if (length > UINT32_MAX - 68)
		return FALSE;

	Stream_Write_UINT32(output, (UINT32)(68 + length));                        /* Length */
	Stream_Write_UINT32(output, 0);                                            /* NextEntryOffset */
	Stream_Write_UINT32(output, 0);                                            /* FileIndex */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwHighDateTime);       /* LastAccessTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime);  /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);                   /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);                  /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);     /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);    /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */
	Stream_Write_UINT32(output, (UINT32)length);                   /* FileNameLength */
	Stream_Write_UINT32(output, 0);                                /* EaSize */
	Stream_Write(output, file->find_data.cFileName, length);
	return TRUE;
}

static BOOL drive_file_query_both_dir_info(DRIVE_FILE* file, wStream* output, size_t length)
{
	WINPR_ASSERT(file);
	WINPR_ASSERT(output);
	/* http://msdn.microsoft.com/en-us/library/cc232095.aspx */
	if (!Stream_EnsureRemainingCapacity(output, 4 + 93 + length))
		return FALSE;

	if (length > UINT32_MAX - 93)
		return FALSE;

	Stream_Write_UINT32(output, (UINT32)(93 + length));                        /* Length */
	Stream_Write_UINT32(output, 0);                                            /* NextEntryOffset */
	Stream_Write_UINT32(output, 0);                                            /* FileIndex */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwLowDateTime); /* CreationTime */
	Stream_Write_UINT32(output, file->find_data.ftCreationTime.dwHighDateTime); /* CreationTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwLowDateTime); /* LastAccessTime */
	Stream_Write_UINT32(output,
	                    file->find_data.ftLastAccessTime.dwHighDateTime);       /* LastAccessTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* LastWriteTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwLowDateTime);  /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.ftLastWriteTime.dwHighDateTime); /* ChangeTime */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);                   /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);                  /* EndOfFile */
	Stream_Write_UINT32(output, file->find_data.nFileSizeLow);     /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.nFileSizeHigh);    /* AllocationSize */
	Stream_Write_UINT32(output, file->find_data.dwFileAttributes); /* FileAttributes */
	Stream_Write_UINT32(output, (UINT32)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, file->find_data.cFileName, length);
	return TRUE;
}

static BOOL drive_file_query_names_info(DRIVE_FILE* file, wStream* output, size_t length)
{
	WINPR_ASSERT(file);
	WINPR_ASSERT(output);
	/* http://msdn.microsoft.com/en-us/library/cc232077.aspx */
	if (!Stream_EnsureRemainingCapacity(output, 4 + 12 + length))
		return FALSE;

	if (length > UINT32_MAX - 12)
		return FALSE;

	Stream_Write_UINT32(output, (UINT32)(12 + length)); /* Length */
	Stream_Write_UINT32(output, 0);                     /* NextEntryOffset */
	Stream_Write_UINT32(output, 0);                     /* FileIndex */
	Stream_Write_UINT32(output, (UINT32)length);        /* FileNameLength */
	Stream_Write(output, file->find_data.cFileName, length);
	return TRUE;
}

BOOL drive_file_query_directory(DRIVE_FILE* file, UINT32 FsInformationClass, BYTE InitialQuery,
                                const WCHAR* path, UINT32 PathWCharLength, wStream* output)
{
	BOOL rc = FALSE;
	size_t length = 0;
	WCHAR* ent_path = nullptr;

	if (!file || !path || !output)
		return FALSE;

	if (InitialQuery != 0)
	{
		/* release search handle */
		if (file->find_handle != INVALID_HANDLE_VALUE)
			FindClose(file->find_handle);

		ent_path = drive_file_combine_fullpath(file->basepath, path, PathWCharLength);
		/* open new search handle and retrieve the first entry */
		file->find_handle = FindFirstFileW(ent_path, &file->find_data);
		free(ent_path);

		if (file->find_handle == INVALID_HANDLE_VALUE)
			goto out_fail;
	}
	else if (!FindNextFileW(file->find_handle, &file->find_data))
		goto out_fail;

	length = _wcslen(file->find_data.cFileName) * sizeof(WCHAR);

	switch (FsInformationClass)
	{
		case FileDirectoryInformation:
			rc = drive_file_query_dir_info(file, output, length);
			break;

		case FileFullDirectoryInformation:
			rc = drive_file_query_full_dir_info(file, output, length);
			break;

		case FileBothDirectoryInformation:
			rc = drive_file_query_both_dir_info(file, output, length);
			break;

		case FileNamesInformation:
			rc = drive_file_query_names_info(file, output, length);
			break;

		default:
			WLog_WARN(TAG, "Unhandled FSInformationClass %s [0x%08" PRIx32 "]",
			          FSInformationClass2Tag(FsInformationClass), FsInformationClass);
			/* Unhandled FsInformationClass */
			goto out_fail;
	}

out_fail:
	if (!rc)
	{
		Stream_Write_UINT32(output, 0); /* Length */
		Stream_Write_UINT8(output, 0);  /* Padding */
	}
	return rc;
}
