/*
 * Copyright (C) 2011 Andrea Mazzoleni
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "portable.h"

#ifdef __MINGW32__ /* Only for MingW */

#include "support.h"

/**
 * Exit codes.
 */
int exit_success = 0;
int exit_failure = 1;
int exit_sync_needed = 2;

/* Add missing Windows declaration */

/* For SetThreadExecutionState */
#ifndef WIN32_ES_SYSTEM_REQUIRED
#define WIN32_ES_SYSTEM_REQUIRED      0x00000001L
#endif
#ifndef WIN32_ES_DISPLAY_REQUIRED
#define WIN32_ES_DISPLAY_REQUIRED     0x00000002L
#endif
#ifndef WIN32_ES_USER_PRESENT
#define WIN32_ES_USER_PRESENT         0x00000004L
#endif
#ifndef WIN32_ES_AWAYMODE_REQUIRED
#define WIN32_ES_AWAYMODE_REQUIRED    0x00000040L
#endif
#ifndef WIN32_ES_CONTINUOUS
#define WIN32_ES_CONTINUOUS           0x80000000L
#endif

/* File Index */
#ifndef FILE_INVALID_FILE_ID
#define FILE_INVALID_FILE_ID          ((ULONGLONG)-1LL)
#endif

/**
 * Direct access to RtlGenRandom().
 * This function is accessible only with LoadLibrary() and it's available from Windows XP.
 */
static BOOLEAN (WINAPI* ptr_RtlGenRandom)(PVOID, ULONG);

/**
 * Direct access to GetTickCount64().
 * This function is available only from Windows Vista.
 */
static ULONGLONG (WINAPI* ptr_GetTickCount64)(void);

/**
 * Description of the last error.
 * It's stored in the thread local storage.
 */
static windows_key_t last_error;

/**
 * Monotone tick counter
 */
static windows_mutex_t tick_lock;
static uint64_t tick_last;

/**
 * If we are running in Wine.
 */
static int is_wine;

/**
 * If we should use the legacy FindFirst/Next way to list directories.
 */
static int is_scan_winfind;

/**
 * Loaded ADVAPI32.DLL.
 */
static HMODULE dll_advapi32;

/**
 * Executable dir.
 *
 * Or empty or terminating with '\'.
 */
static WCHAR exedir[MAX_PATH];

/**
 * Set the executable dir.
 */
static void exedir_init(void)
{
	DWORD size;
	WCHAR* slash;

	size = GetModuleFileNameW(0, exedir, MAX_PATH);
	if (size == 0 || size == MAX_PATH) {
		/* use empty dir */
		exedir[0] = 0;
		return;
	}

	slash = wcsrchr(exedir, L'\\');
	if (!slash) {
		/* use empty dir */
		exedir[0] = 0;
		return;
	}

	/* cut exe name */
	slash[1] = 0;
}

void os_init(int opt)
{
	HMODULE ntdll, kernel32;

	is_scan_winfind = opt != 0;

	/* initialize the thread local storage for strerror(), using free() as destructor */
	if (windows_key_create(&last_error, free) != 0) {
		log_fatal("Error calling windows_key_create().\n");
		exit(EXIT_FAILURE);
	}

	tick_last = 0;
	if (windows_mutex_init(&tick_lock, 0) != 0) {
		log_fatal("Error calling windows_mutex_init().\n");
		exit(EXIT_FAILURE);
	}

	ntdll = GetModuleHandle("NTDLL.DLL");
	if (!ntdll) {
		log_fatal("Error loading the NTDLL module.\n");
		exit(EXIT_FAILURE);
	}

	kernel32 = GetModuleHandle("KERNEL32.DLL");
	if (!kernel32) {
		log_fatal("Error loading the KERNEL32 module.\n");
		exit(EXIT_FAILURE);
	}

	dll_advapi32 = LoadLibrary("ADVAPI32.DLL");
	if (!dll_advapi32) {
		log_fatal("Error loading the ADVAPI32 module.\n");
		exit(EXIT_FAILURE);
	}

	/* check for Wine presence */
	is_wine = GetProcAddress(ntdll, "wine_get_version") != 0;

	/* setup the standard random generator used as fallback */
	srand(GetTickCount());

	/* get pointer to RtlGenRandom, note that it was reported missing in some cases */
	ptr_RtlGenRandom = (void*)GetProcAddress(dll_advapi32, "SystemFunction036");

	/* get pointer to RtlGenRandom, note that it was reported missing in some cases */
	ptr_GetTickCount64 = (void*)GetProcAddress(kernel32, "GetTickCount64");

	/* set the thread execution level to avoid sleep */
	/* first try for Windows 7 */
	if (SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED | WIN32_ES_AWAYMODE_REQUIRED) == 0) {
		/* retry with the XP variant */
		SetThreadExecutionState(WIN32_ES_CONTINUOUS | WIN32_ES_SYSTEM_REQUIRED);
	}

	exedir_init();
}

void os_done(void)
{
	/* delete the thread local storage for strerror() */
	windows_key_delete(last_error);

	windows_mutex_destroy(&tick_lock);

	/* restore the normal execution level */
	SetThreadExecutionState(WIN32_ES_CONTINUOUS);

	FreeLibrary(dll_advapi32);
}

void os_abort(void)
{
	void* stack[32];
	size_t size;
	unsigned i;

	printf("Stacktrace of " PACKAGE " v" VERSION);
	printf(", mingw");
#ifdef __GNUC__
	printf(", gcc " __VERSION__);
#endif
	printf(", %d-bit", (int)sizeof(void *) * 8);
	printf(", PATH_MAX=%d", PATH_MAX);
	printf("\n");

	/* get stackstrace, but without symbols */
	size = CaptureStackBackTrace(0, 32, stack, NULL);

	for (i = 0; i < size; ++i)
		printf("[bt] %02u: %p\n", i, stack[i]);

	printf("Please report this error to the SnapRAID Forum:\n");
	printf("https://sourceforge.net/p/snapraid/discussion/1677233/\n");

	/* use exit() and not abort to avoid the Windows abort dialog */
	exit(EXIT_FAILURE);
}

void os_clear(void)
{
	HANDLE console;
	CONSOLE_SCREEN_BUFFER_INFO screen;
	COORD coord;
	DWORD written;

	/* get the console */
	console = GetStdHandle(STD_OUTPUT_HANDLE);
	if (console == INVALID_HANDLE_VALUE)
		return;

	/* get the screen size */
	if (!GetConsoleScreenBufferInfo(console, &screen))
		return;

	/* fill the screen with spaces */
	coord.X = 0;
	coord.Y = 0;
	FillConsoleOutputCharacterA(console, ' ', screen.dwSize.X * screen.dwSize.Y, coord, &written);

	/* set the cursor at the top left corner */
	SetConsoleCursorPosition(console, coord);
}

/**
 * Size in chars of conversion buffers for u8to16() and u16to8().
 */
#define CONV_MAX PATH_MAX

/**
 * Convert a generic string from UTF8 to UTF16.
 */
static wchar_t* u8tou16(wchar_t* conv_buf, const char* src)
{
	int ret;

	ret = MultiByteToWideChar(CP_UTF8, 0, src, -1, conv_buf, CONV_MAX);

	if (ret <= 0) {
		log_fatal("Error converting name '%s' from UTF-8 to UTF-16\n", src);
		exit(EXIT_FAILURE);
	}

	return conv_buf;
}

/**
 * Convert a generic string from UTF16 to UTF8.
 */
static char* u16tou8ex(char* conv_buf, const wchar_t* src, size_t number_of_wchar, size_t* result_length_without_terminator)
{
	int ret;

	ret = WideCharToMultiByte(CP_UTF8, 0, src, number_of_wchar, conv_buf, CONV_MAX, 0, 0);
	if (ret <= 0) {
		log_fatal("Error converting from UTF-16 to UTF-8\n");
		exit(EXIT_FAILURE);
	}

	*result_length_without_terminator = ret;

	return conv_buf;
}

static char* u16tou8(char* conv_buf, const wchar_t* src)
{
	size_t len;

	/* convert also the 0 terminator */
	return u16tou8ex(conv_buf, src, wcslen(src) + 1, &len);
}

/**
 * Check if the char is a forward or back slash.
 */
static int is_slash(char c)
{
	return c == '/' || c == '\\';
}

/**
 * Convert a path to the Windows format.
 *
 * If only_is_required is 1, the extended-length format is used only if required.
 *
 * The exact operation done is:
 * - If it's a '\\?\' or '\\.\' path, convert any '/' to '\'.
 * - If it's a disk designator path, like 'D:\' or 'D:/', it prepends '\\?\' to the path and convert any '/' to '\'.
 * - If it's a UNC path, like ''\\server'', it prepends '\\?\UNC\' to the path and convert any '/' to '\'.
 * - Otherwise, only the UTF conversion is done. In this case Windows imposes a limit of 260 chars, and automatically convert any '/' to '\'.
 *
 * For more details see:
 * Naming Files, Paths, and Namespaces
 * http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx#maxpath
 */
static wchar_t* convert_arg(wchar_t* conv_buf, const char* src, int only_if_required)
{
	int ret;
	wchar_t* dst;
	int count;

	dst = conv_buf;

	/* note that we always check for both / and \ because the path is blindly */
	/* converted to unix format by path_import() */

	if (only_if_required && strlen(src) < 260 - 12) {
		/* it's a short path */
		/* 260 is the MAX_PATH, note that it includes the space for the terminating NUL */
		/* 12 is an additional space for filename, required when creating directory */

		/* do nothing */
	} else if (is_slash(src[0]) && is_slash(src[1]) && (src[2] == '?' || src[2] == '.') && is_slash(src[3])) {
		/* if it's already a '\\?\' or '\\.\' path */

		/* do nothing */
	} else if (is_slash(src[0]) && is_slash(src[1])) {
		/* if it is a UNC path, like '\\server' */

		/* prefix with '\\?\UNC\' */
		*dst++ = L'\\';
		*dst++ = L'\\';
		*dst++ = L'?';
		*dst++ = L'\\';
		*dst++ = L'U';
		*dst++ = L'N';
		*dst++ = L'C';
		*dst++ = L'\\';

		/* skip initial '\\' */
		src += 2;
	} else if (src[0] != 0 && src[1] == ':' && is_slash(src[2])) {
		/* if it is a disk designator path, like 'D:\' or 'D:/' */

		/* prefix with '\\?\' */
		*dst++ = L'\\';
		*dst++ = L'\\';
		*dst++ = L'?';
		*dst++ = L'\\';
	}

	/* chars already used */
	count = dst - conv_buf;

	ret = MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, CONV_MAX - count);

	if (ret <= 0) {
		log_fatal("Error converting name '%s' from UTF-8 to UTF-16\n", src);
		exit(EXIT_FAILURE);
	}

	/* convert any / to \ */
	/* note that in UTF-16, it's not possible to have '/' used as part */
	/* of a pair of codes representing a single UNICODE char */
	/* See: http://en.wikipedia.org/wiki/UTF-16 */
	while (*dst) {
		if (*dst == L'/')
			*dst = L'\\';
		++dst;
	}

	return conv_buf;
}

#define convert(buf, a) convert_arg(buf, a, 0)
#define convert_if_required(buf, a) convert_arg(buf, a, 1)

static BOOL GetReparseTagInfoByHandle(HANDLE hFile, FILE_ATTRIBUTE_TAG_INFO* lpFileAttributeTagInfo, DWORD dwFileAttributes)
{
	/* if not a reparse point, return no info */
	if ((dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
		lpFileAttributeTagInfo->FileAttributes = dwFileAttributes;
		lpFileAttributeTagInfo->ReparseTag = 0;
		return TRUE;
	}

	/* do the real call */
	return GetFileInformationByHandleEx(hFile, FileAttributeTagInfo, lpFileAttributeTagInfo, sizeof(FILE_ATTRIBUTE_TAG_INFO));
}

/**
 * Convert Windows attr to the Unix stat format.
 */
static void windows_attr2stat(DWORD FileAttributes, DWORD ReparseTag, struct windows_stat* st)
{
	/* Convert special attributes */
	if ((FileAttributes & FILE_ATTRIBUTE_DEVICE) != 0) {
		st->st_mode = S_IFBLK;
		st->st_desc = "device";
	} else if ((FileAttributes & FILE_ATTRIBUTE_OFFLINE) != 0) { /* Offline */
		st->st_mode = S_IFCHR;
		st->st_desc = "offline";
	} else if ((FileAttributes & FILE_ATTRIBUTE_TEMPORARY) != 0) { /* Temporary */
		st->st_mode = S_IFCHR;
		st->st_desc = "temporary";
	} else if ((FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { /* Reparse point */
		switch (ReparseTag) {
		/* if we don't have the ReparseTag information */
		case 0 :
			/* don't set the st_mode, to set it later calling lstat_sync() */
			st->st_mode = 0;
			st->st_desc = "unknown";
			break;
		/* for deduplicated files, assume that they are regular ones */
		case IO_REPARSE_TAG_DEDUP :
			if ((FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
				st->st_mode = S_IFDIR;
				st->st_desc = "directory-dedup";
			} else {
				st->st_mode = S_IFREG;
				st->st_desc = "regular-dedup";
			}
			break;
		case IO_REPARSE_TAG_SYMLINK :
			if ((FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
				st->st_mode = S_IFLNKDIR;
				st->st_desc = "reparse-point-symlink-dir";
			} else {
				st->st_mode = S_IFLNK;
				st->st_desc = "reparse-point-symlink-file";
			}
			break;
		/* all the other are skipped as reparse-point */
		case IO_REPARSE_TAG_MOUNT_POINT :
			st->st_mode = S_IFCHR;
			st->st_desc = "reparse-point-mount";
			break;
		case IO_REPARSE_TAG_NFS :
			st->st_mode = S_IFCHR;
			st->st_desc = "reparse-point-nfs";
			break;
		default :
			st->st_mode = S_IFCHR;
			st->st_desc = "reparse-point";
			break;
		}
	} else if ((FileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0) { /* System */
		if ((FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
			st->st_mode = S_IFCHR;
			st->st_desc = "system-directory";
		} else {
			st->st_mode = S_IFREG;
			st->st_desc = "system-file";
		}
	} else {
		if ((FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
			st->st_mode = S_IFDIR;
			st->st_desc = "directory";
		} else {
			st->st_mode = S_IFREG;
			st->st_desc = "regular";
		}
	}

	/* store the HIDDEN attribute in a separate field */
	st->st_hidden = (FileAttributes & FILE_ATTRIBUTE_HIDDEN) != 0;
}

/**
 * Convert Windows info to the Unix stat format.
 */
static int windows_info2stat(const BY_HANDLE_FILE_INFORMATION* info, const FILE_ATTRIBUTE_TAG_INFO* tag, struct windows_stat* st)
{
	int64_t mtime;

	windows_attr2stat(info->dwFileAttributes, tag->ReparseTag, st);

	st->st_size = info->nFileSizeHigh;
	st->st_size <<= 32;
	st->st_size |= info->nFileSizeLow;

	mtime = info->ftLastWriteTime.dwHighDateTime;
	mtime <<= 32;
	mtime |= info->ftLastWriteTime.dwLowDateTime;

	/*
	 * Convert to unix time
	 *
	 * How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
	 * http://support.microsoft.com/kb/167296
	 */
	mtime -= 116444736000000000LL;
	st->st_mtime = mtime / 10000000;
	st->st_mtimensec = (mtime % 10000000) * 100;

	st->st_ino = info->nFileIndexHigh;
	st->st_ino <<= 32;
	st->st_ino |= info->nFileIndexLow;

	st->st_nlink = info->nNumberOfLinks;

	st->st_dev = info->dwVolumeSerialNumber;

	/* GetFileInformationByHandle() ensures to return synced information */
	st->st_sync = 1;

	/**
	 * In ReFS the IDs are 128 bit, and the 64 bit interface may fail.
	 *
	 * From Microsoft "Application Compatibility with ReFS"
	 * http://download.microsoft.com/download/C/B/3/CB3561DC-6BF6-443D-B5B9-9676ACDF7F75/Application%20Compatibility%20with%20ReFS.docx
	 * "64-bit file identifier can be obtained from GetFileInformationByHandle in"
	 * "the nFileIndexHigh and nFileIndexLow members. This API is an extended version"
	 * "that includes 128-bit file identifiers.  If GetFileInformationByHandle returns"
	 * "FILE_INVALID_FILE_ID, the identifier may only be described in 128 bit form."
	 */
	if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID) {
		log_fatal("Invalid inode number! Is this ReFS?\n");
		errno = EINVAL;
		return -1;
	}

	return 0;
}

/**
 * Convert Windows info to the Unix stat format.
 */
static int windows_stream2stat(const BY_HANDLE_FILE_INFORMATION* info, const FILE_ID_BOTH_DIR_INFO* stream, struct windows_stat* st)
{
	int64_t mtime;

	/* The FILE_ID_BOTH_DIR_INFO doesn't have the ReparseTag information */
	/* we could use instead FILE_ID_EXTD_DIR_INFO, but it's available only */
	/* from Windows Server 2012 */
	windows_attr2stat(stream->FileAttributes, 0, st);

	st->st_size = stream->EndOfFile.QuadPart;

	mtime = stream->LastWriteTime.QuadPart;

	/*
	 * Convert to unix time
	 *
	 * How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
	 * http://support.microsoft.com/kb/167296
	 */
	mtime -= 116444736000000000LL;
	st->st_mtime = mtime / 10000000;
	st->st_mtimensec = (mtime % 10000000) * 100;

	st->st_ino = stream->FileId.QuadPart;

	st->st_nlink = info->nNumberOfLinks;

	st->st_dev = info->dwVolumeSerialNumber;

	/* directory listing doesn't ensure to return synced information */
	st->st_sync = 0;

	/* in ReFS the IDs are 128 bit, and the 64 bit interface may fail */
	if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID) {
		log_fatal("Invalid inode number! Is this ReFS?\n");
		errno = EINVAL;
		return -1;
	}

	return 0;
}

/**
 * Convert Windows findfirst info to the Unix stat format.
 */
static void windows_finddata2stat(const WIN32_FIND_DATAW* info, struct windows_stat* st)
{
	int64_t mtime;

	windows_attr2stat(info->dwFileAttributes, info->dwReserved0, st);

	st->st_size = info->nFileSizeHigh;
	st->st_size <<= 32;
	st->st_size |= info->nFileSizeLow;

	mtime = info->ftLastWriteTime.dwHighDateTime;
	mtime <<= 32;
	mtime |= info->ftLastWriteTime.dwLowDateTime;

	/*
	 * Convert to unix time
	 *
	 * How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
	 * http://support.microsoft.com/kb/167296
	 */
	mtime -= 116444736000000000LL;
	st->st_mtime = mtime / 10000000;
	st->st_mtimensec = (mtime % 10000000) * 100;

	/* No inode information available */
	st->st_ino = 0;

	/* No link information available */
	st->st_nlink = 0;

	/* No device information available */
	st->st_dev = 0;

	/* directory listing doesn't ensure to return synced information */
	st->st_sync = 0;
}

static void windows_finddata2dirent(const WIN32_FIND_DATAW* info, struct windows_dirent* dirent)
{
	char conv_buf[CONV_MAX];
	const char* name;
	size_t len;

	name = u16tou8ex(conv_buf, info->cFileName, wcslen(info->cFileName), &len);

	if (len + 1 >= sizeof(dirent->d_name)) {
		log_fatal("Name too long\n");
		exit(EXIT_FAILURE);
	}

	memcpy(dirent->d_name, name, len);
	dirent->d_name[len] = 0;

	windows_finddata2stat(info, &dirent->d_stat);
}

static int windows_stream2dirent(const BY_HANDLE_FILE_INFORMATION* info, const FILE_ID_BOTH_DIR_INFO* stream, struct windows_dirent* dirent)
{
	char conv_buf[CONV_MAX];
	const char* name;
	size_t len;

	name = u16tou8ex(conv_buf, stream->FileName, stream->FileNameLength / 2, &len);

	if (len + 1 >= sizeof(dirent->d_name)) {
		log_fatal("Name too long\n");
		exit(EXIT_FAILURE);
	}

	memcpy(dirent->d_name, name, len);
	dirent->d_name[len] = 0;

	return windows_stream2stat(info, stream, &dirent->d_stat);
}

/**
 * Convert Windows error to errno.
 */
static void windows_errno(DWORD error)
{
	switch (error) {
	case ERROR_INVALID_HANDLE :
		/* we check for a bad handle calling _get_osfhandle() */
		/* and in such case we return EBADF */
		/* Other cases are here identified with EINVAL */
		errno = EINVAL;
		break;
	case ERROR_HANDLE_EOF : /* in ReadFile() over the end of the file */
		errno = EINVAL;
		break;
	case ERROR_FILE_NOT_FOUND :
	case ERROR_PATH_NOT_FOUND : /* in GetFileAttributeW() if internal path not found */
		errno = ENOENT;
		break;
	case ERROR_ACCESS_DENIED : /* in CreateDirectoryW() if dir is scheduled for deletion */
	case ERROR_CURRENT_DIRECTORY : /* in RemoveDirectoryW() if removing the current directory */
	case ERROR_SHARING_VIOLATION : /* in RemoveDirectoryW() if in use */
	case ERROR_WRITE_PROTECT : /* when dealing with read-only media/snapshot and trying to write to them */
		errno = EACCES;
		break;
	case ERROR_ALREADY_EXISTS : /* in CreateDirectoryW() if already exists */
		errno = EEXIST;
		break;
	case ERROR_DISK_FULL :
		errno = ENOSPC;
		break;
	case ERROR_BUFFER_OVERFLOW :
		errno = ENAMETOOLONG;
		break;
	case ERROR_NOT_ENOUGH_MEMORY :
		errno = ENOMEM;
		break;
	case ERROR_NOT_SUPPORTED : /* in CreateSymlinkW() if not present in kernel32 */
		errno = ENOSYS;
		break;
	case ERROR_PRIVILEGE_NOT_HELD : /* in CreateSymlinkW() if no SeCreateSymbolicLinkPrivilige permission */
		errno = EPERM;
		break;
	case ERROR_IO_DEVICE : /* in ReadFile() and WriteFile() */
	case ERROR_CRC : /* in ReadFile() */
		errno = EIO;
		break;
	default :
		log_fatal("Unexpected Windows error %d.\n", (int)error);
		errno = EIO;
		break;
	}
}

int windows_fstat(int fd, struct windows_stat* st)
{
	BY_HANDLE_FILE_INFORMATION info;
	FILE_ATTRIBUTE_TAG_INFO tag;
	HANDLE h;

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	if (!GetFileInformationByHandle(h, &info)) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!GetReparseTagInfoByHandle(h, &tag, info.dwFileAttributes)) {
		windows_errno(GetLastError());
		return -1;
	}

	return windows_info2stat(&info, &tag, st);
}

int windows_lstat(const char* file, struct windows_stat* st)
{
	wchar_t conv_buf[CONV_MAX];
	HANDLE h;
	WIN32_FIND_DATAW data;

	/* FindFirstFileW by default gets information of symbolic links and not of their targets */
	h = FindFirstFileW(convert(conv_buf, file), &data);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!FindClose(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	windows_finddata2stat(&data, st);

	return 0;
}

void windows_dirent_lstat(const struct windows_dirent* dd, struct windows_stat* st)
{
	memcpy(st, &dd->d_stat, sizeof(struct windows_stat));
}

int windows_mkdir(const char* file)
{
	wchar_t conv_buf[CONV_MAX];

	if (!CreateDirectoryW(convert(conv_buf, file), 0)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

int windows_rmdir(const char* file)
{
	wchar_t conv_buf[CONV_MAX];

	if (!RemoveDirectoryW(convert(conv_buf, file))) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

static BOOL GetFilePhysicalOffset(HANDLE h, uint64_t* physical)
{
	STARTING_VCN_INPUT_BUFFER svib;
	unsigned char rpb_buffer[sizeof(RETRIEVAL_POINTERS_BUFFER)];
	RETRIEVAL_POINTERS_BUFFER* rpb = (RETRIEVAL_POINTERS_BUFFER*)&rpb_buffer;
	BOOL ret;
	DWORD n;

	/* in Wine FSCTL_GET_RETRIEVAL_POINTERS is not supported */
	if (is_wine) {
		*physical = FILEPHY_UNREPORTED_OFFSET;
		return TRUE;
	}

	/* set the output variable, just to be safe */
	rpb->ExtentCount = 0;

	/* read the physical address */
	svib.StartingVcn.QuadPart = 0;
	ret = DeviceIoControl(h, FSCTL_GET_RETRIEVAL_POINTERS, &svib, sizeof(svib), rpb_buffer, sizeof(rpb_buffer), &n, 0);
	if (!ret) {
		DWORD error = GetLastError();
		if (error == ERROR_MORE_DATA) {
			/* we ignore ERROR_MODE_DATA because we are interested only at the first entry */
			/* and this is the expected error if the files has more entries */
		} else if (error == ERROR_HANDLE_EOF) {
			/* if the file is small, it can be stored in the Master File Table (MFT) */
			/* and then it doesn't have a physical address */
			/* In such case we report a specific fake address, to report this special condition */
			/* that it's different from the 0 offset reported by the underline file system */
			*physical = FILEPHY_WITHOUT_OFFSET;
			return TRUE;
		} else if (error == ERROR_NOT_SUPPORTED) {
			/* for disks shared on network this operation is not supported */
			*physical = FILEPHY_UNREPORTED_OFFSET;
			return TRUE;
		} else {
			return FALSE;
		}
	}

	if (rpb->ExtentCount < 1)
		*physical = FILEPHY_UNREPORTED_OFFSET;
	else
		*physical = rpb->Extents[0].Lcn.QuadPart + FILEPHY_REAL_OFFSET;

	return TRUE;
}

int lstat_sync(const char* file, struct windows_stat* st, uint64_t* physical)
{
	wchar_t conv_buf[CONV_MAX];
	BY_HANDLE_FILE_INFORMATION info;
	FILE_ATTRIBUTE_TAG_INFO tag;
	HANDLE h;

	/*
	 * Open the handle of the file.
	 *
	 * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files).
	 * Use FILE_FLAG_OPEN_REPARSE_POINT to open symbolic links and not the their target.
	 *
	 * Note that even with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
	 * and FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT some paths
	 * cannot be opened like "C:\System Volume Information" resulting
	 * in error ERROR_ACCESS_DENIED.
	 */
	h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!GetFileInformationByHandle(h, &info)) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	if (!GetReparseTagInfoByHandle(h, &tag, info.dwFileAttributes)) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	/* read the physical offset, only if a pointer is provided */
	if (physical != 0) {
		if (!GetFilePhysicalOffset(h, physical)) {
			DWORD error = GetLastError();
			CloseHandle(h);
			windows_errno(error);
			return -1;
		}
	}

	if (!CloseHandle(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	return windows_info2stat(&info, &tag, st);
}

int windows_stat(const char* file, struct windows_stat* st)
{
	wchar_t conv_buf[CONV_MAX];
	BY_HANDLE_FILE_INFORMATION info;
	FILE_ATTRIBUTE_TAG_INFO tag;
	HANDLE h;

	/*
	 * Open the handle of the file.
	 *
	 * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files).
	 */
	h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!GetFileInformationByHandle(h, &info)) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	if (!GetReparseTagInfoByHandle(h, &tag, info.dwFileAttributes)) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	if (!CloseHandle(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	return windows_info2stat(&info, &tag, st);
}

int windows_ftruncate(int fd, off64_t off)
{
	HANDLE h;
	LARGE_INTEGER pos;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	pos.QuadPart = off;
	if (!SetFilePointerEx(h, pos, 0, FILE_BEGIN)) {
		windows_errno(GetLastError());
		return -1;
	}

	/*
	 * Windows effectively reserves space, but it doesn't initialize it.
	 * It's then important to write starting from the begin to the end,
	 * to avoid to have Windows to fill the holes writing zeros.
	 *
	 * See:
	 * "Why does my single-byte write take forever?"
	 * http://blogs.msdn.com/b/oldnewthing/archive/2011/09/22/10215053.aspx
	 */
	if (!SetEndOfFile(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

int windows_fallocate(int fd, int mode, off64_t off, off64_t len)
{
	if (mode != 0)
		return -1;

	/* no difference with ftruncate because Windows doesn't use sparse files */
	return windows_ftruncate(fd, off + len);
}

int windows_fsync(int fd)
{
	HANDLE h;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	/*
	 * "The FlushFileBuffers API can be used to flush all the outstanding data
	 * and metadata on a single file or a whole volume. However, frequent use
	 * of this API can cause reduced throughput. Internally, Windows uses the
	 * SCSI Synchronize Cache or the IDE/ATAPI Flush cache commands."
	 *
	 * From:
	 * "Windows Write Caching - Part 2 An overview for Application Developers"
	 * http://winntfs.com/2012/11/29/windows-write-caching-part-2-an-overview-for-application-developers/
	 */
	if (!FlushFileBuffers(h)) {
		DWORD error = GetLastError();

		switch (error) {
		case ERROR_INVALID_HANDLE :
			/*
			 * FlushFileBuffers returns this error if the handle
			 * doesn't support buffering, like the console output.
			 *
			 * We had a report that also ATA-over-Ethernet returns
			 * this error, but not enough sure to ignore it.
			 * So, we use now an extended error reporting.
			 */
			log_fatal("Unexpected Windows INVALID_HANDLE error in FlushFileBuffers().\n");
			log_fatal("Are you using ATA-over-Ethernet ? Please report it.\n");

			/* normal error processing */
			windows_errno(error);
			return -1;

		case ERROR_ACCESS_DENIED :
			/*
			 * FlushFileBuffers returns this error for read-only
			 * data, that cannot have to be flushed.
			 */
			return 0;

		default :
			windows_errno(error);
			return -1;
		}
	}

	return 0;
}

int windows_futimens(int fd, struct windows_timespec tv[2])
{
	HANDLE h;
	FILETIME ft;
	int64_t mtime;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	/*
	 * Convert to windows time
	 *
	 * How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
	 * http://support.microsoft.com/kb/167296
	 */
	mtime = tv[0].tv_sec;
	mtime *= 10000000;
	mtime += tv[0].tv_nsec / 100;
	mtime += 116444736000000000LL;

	ft.dwHighDateTime = mtime >> 32;
	ft.dwLowDateTime = mtime;

	if (!SetFileTime(h, 0, 0, &ft)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

int windows_utimensat(int fd, const char* file, struct windows_timespec tv[2], int flags)
{
	wchar_t conv_buf[CONV_MAX];
	HANDLE h;
	FILETIME ft;
	int64_t mtime;
	DWORD wflags;

	/*
	 * Support only the absolute paths
	 */
	if (fd != AT_FDCWD) {
		errno = EBADF;
		return -1;
	}

	/*
	 * Open the handle of the file.
	 *
	 * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files).
	 * Use FILE_FLAG_OPEN_REPARSE_POINT to open symbolic links and not the their target.
	 *
	 * Note that even with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
	 * and FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT some paths
	 * cannot be opened like "C:\System Volume Information" resulting
	 * in error ERROR_ACCESS_DENIED.
	 */
	wflags = FILE_FLAG_BACKUP_SEMANTICS;
	if ((flags & AT_SYMLINK_NOFOLLOW) != 0)
		wflags |= FILE_FLAG_OPEN_REPARSE_POINT;
	h = CreateFileW(convert(conv_buf, file), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, wflags, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	/*
	 * Convert to windows time
	 *
	 * How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
	 * http://support.microsoft.com/kb/167296
	 */
	mtime = tv[0].tv_sec;
	mtime *= 10000000;
	mtime += tv[0].tv_nsec / 100;
	mtime += 116444736000000000LL;

	ft.dwHighDateTime = mtime >> 32;
	ft.dwLowDateTime = mtime;

	if (!SetFileTime(h, 0, 0, &ft)) {
		windows_errno(GetLastError());
		CloseHandle(h);
		return -1;
	}

	if (!CloseHandle(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

int windows_rename(const char* from, const char* to)
{
	wchar_t conv_buf_from[CONV_MAX];
	wchar_t conv_buf_to[CONV_MAX];

	/*
	 * Implements an atomic rename in Windows.
	 * Not really atomic at now to support XP.
	 *
	 * Is an atomic file rename (with overwrite) possible on Windows?
	 * http://stackoverflow.com/questions/167414/is-an-atomic-file-rename-with-overwrite-possible-on-windows
	 */
	if (!MoveFileExW(convert(conv_buf_from, from), convert(conv_buf_to, to), MOVEFILE_REPLACE_EXISTING)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

int windows_remove(const char* file)
{
	wchar_t conv_buf[CONV_MAX];

	if (!DeleteFileW(convert(conv_buf, file))) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

FILE* windows_fopen(const char* file, const char* mode)
{
	wchar_t conv_buf_file[CONV_MAX];
	wchar_t conv_buf_mode[CONV_MAX];

	return _wfopen(convert(conv_buf_file, file), u8tou16(conv_buf_mode, mode));
}

int windows_open(const char* file, int flags, ...)
{
	wchar_t conv_buf[CONV_MAX];
	HANDLE h;
	int f;
	DWORD access;
	DWORD share;
	DWORD create;
	DWORD attr;

	switch (flags & O_ACCMODE) {
	case O_RDONLY :
		access = GENERIC_READ;
		break;
	case O_WRONLY :
		access = GENERIC_WRITE;
		break;
	case O_RDWR :
		access = GENERIC_READ | GENERIC_WRITE;
		break;
	default:
		errno = EINVAL;
		return -1;
	}

	share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;

	switch (flags & (O_CREAT | O_EXCL | O_TRUNC)) {
	case 0 :
		create = OPEN_EXISTING;
		break;
	case O_CREAT :
		create = OPEN_ALWAYS;
		break;
	case O_CREAT | O_EXCL :
	case O_CREAT | O_EXCL | O_TRUNC :
		create = CREATE_NEW;
		break;
	case O_CREAT | O_TRUNC :
		create = CREATE_ALWAYS;
		break;
	case O_TRUNC :
		create = TRUNCATE_EXISTING;
		break;
	default:
		errno = EINVAL;
		return -1;
	}

	attr = FILE_ATTRIBUTE_NORMAL;
	if ((flags & O_DIRECT) != 0)
		attr |= FILE_FLAG_NO_BUFFERING;
	if ((flags & O_DSYNC) != 0)
		attr |= FILE_FLAG_WRITE_THROUGH;
	if ((flags & O_RANDOM) != 0)
		attr |= FILE_FLAG_RANDOM_ACCESS;
	if ((flags & O_SEQUENTIAL) != 0)
		attr |= FILE_FLAG_SEQUENTIAL_SCAN;
	if ((flags & _O_SHORT_LIVED) != 0)
		attr |= FILE_ATTRIBUTE_TEMPORARY;
	if ((flags & O_TEMPORARY) != 0)
		attr |= FILE_FLAG_DELETE_ON_CLOSE;

	h = CreateFileW(convert(conv_buf, file), access, share, 0, create, attr, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	/* mask out flags unknown by Windows */
	flags &= ~(O_DIRECT | O_DSYNC);

	f = _open_osfhandle((intptr_t)h, flags);
	if (f == -1) {
		CloseHandle(h);
		return -1;
	}

	return f;
}

struct windows_dir_struct {
	BY_HANDLE_FILE_INFORMATION info;
	WIN32_FIND_DATAW find;
	HANDLE h;
	struct windows_dirent entry;
	unsigned char* buffer;
	unsigned buffer_size;
	unsigned buffer_pos;
	int state;
};

#define DIR_STATE_EOF -1 /**< End of the dir stream */
#define DIR_STATE_EMPTY 0 /**< The entry is empty. */
#define DIR_STATE_FILLED 1 /**< The entry is valid. */

static windows_dir* windows_opendir_find(const char* dir)
{
	wchar_t conv_buf[CONV_MAX];
	wchar_t* wdir;
	windows_dir* dirstream;
	size_t len;

	dirstream = malloc(sizeof(windows_dir));
	if (!dirstream) {
		log_fatal("Low memory\n");
		exit(EXIT_FAILURE);
	}

	wdir = convert(conv_buf, dir);

	/* add final / and * */
	len = wcslen(wdir);
	if (len != 0 && wdir[len - 1] != '\\')
		wdir[len++] = L'\\';
	wdir[len++] = L'*';
	wdir[len++] = 0;

	dirstream->h = FindFirstFileW(wdir, &dirstream->find);
	if (dirstream->h == INVALID_HANDLE_VALUE) {
		DWORD error = GetLastError();

		if (error == ERROR_FILE_NOT_FOUND) {
			dirstream->state = DIR_STATE_EOF;
			return dirstream;
		}

		free(dirstream);
		windows_errno(error);
		return 0;
	}

	windows_finddata2dirent(&dirstream->find, &dirstream->entry);
	dirstream->state = DIR_STATE_FILLED;

	return dirstream;
}

static struct windows_dirent* windows_readdir_find(windows_dir* dirstream)
{
	if (dirstream->state == DIR_STATE_EMPTY) {
		if (!FindNextFileW(dirstream->h, &dirstream->find)) {
			DWORD error = GetLastError();

			if (error != ERROR_NO_MORE_FILES) {
				windows_errno(error);
				return 0;
			}

			dirstream->state = DIR_STATE_EOF;
		} else {
			windows_finddata2dirent(&dirstream->find, &dirstream->entry);
			dirstream->state = DIR_STATE_FILLED;
		}
	}

	if (dirstream->state == DIR_STATE_FILLED) {
		dirstream->state = DIR_STATE_EMPTY;
		return &dirstream->entry;
	}

	/* otherwise it's the end of stream */
	assert(dirstream->state == DIR_STATE_EOF);
	errno = 0;

	return 0;
}

static int windows_closedir_find(windows_dir* dirstream)
{
	if (dirstream->h != INVALID_HANDLE_VALUE) {
		if (!FindClose(dirstream->h)) {
			DWORD error = GetLastError();

			free(dirstream);

			windows_errno(error);
			return -1;
		}
	}

	free(dirstream);

	return 0;
}

static int windows_first_stream(windows_dir* dirstream)
{
	FILE_ID_BOTH_DIR_INFO* fd;

	if (!GetFileInformationByHandleEx(dirstream->h, FileIdBothDirectoryInfo, dirstream->buffer, dirstream->buffer_size)) {
		DWORD error = GetLastError();

		if (error == ERROR_NO_MORE_FILES) {
			dirstream->state = DIR_STATE_EOF;
			return 0;
		}

		windows_errno(error);
		return -1;
	}

	/* get the first entry */
	dirstream->state = DIR_STATE_FILLED;
	dirstream->buffer_pos = 0;
	fd = (FILE_ID_BOTH_DIR_INFO*)dirstream->buffer;
	return windows_stream2dirent(&dirstream->info, fd, &dirstream->entry);
}

static int windows_next_stream(windows_dir* dirstream)
{
	FILE_ID_BOTH_DIR_INFO* fd;

	/* last entry read */
	fd = (FILE_ID_BOTH_DIR_INFO*)(dirstream->buffer + dirstream->buffer_pos);

	/* check if there is a next one */
	if (fd->NextEntryOffset == 0) {
		/* if not, fill it up again */
		if (windows_first_stream(dirstream) != 0)
			return -1;
		return 0;
	}

	/* go to the next one */
	dirstream->state = DIR_STATE_FILLED;
	dirstream->buffer_pos += fd->NextEntryOffset;
	fd = (FILE_ID_BOTH_DIR_INFO*)(dirstream->buffer + dirstream->buffer_pos);
	return windows_stream2dirent(&dirstream->info, fd, &dirstream->entry);
}

static windows_dir* windows_opendir_stream(const char* dir)
{
	wchar_t conv_buf[CONV_MAX];
	windows_dir* dirstream;
	WCHAR* wdir;

	dirstream = malloc(sizeof(windows_dir));
	if (!dirstream) {
		log_fatal("Low memory\n");
		exit(EXIT_FAILURE);
	}

	wdir = convert(conv_buf, dir);

	/* uses a 64 kB buffer for reading directory */
	dirstream->buffer_size = 64 * 1024;
	dirstream->buffer = malloc(dirstream->buffer_size);
	if (!dirstream->buffer) {
		log_fatal("Low memory\n");
		exit(EXIT_FAILURE);
	}

	dirstream->h = CreateFileW(wdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
	if (dirstream->h == INVALID_HANDLE_VALUE) {
		DWORD error = GetLastError();
		free(dirstream->buffer);
		free(dirstream);
		windows_errno(error);
		return 0;
	}

	/* get dir information for the VolumeSerialNumber */
	/* this value is used for all the files in the dir */
	if (!GetFileInformationByHandle(dirstream->h, &dirstream->info)) {
		DWORD error = GetLastError();
		CloseHandle(dirstream->h);
		free(dirstream->buffer);
		free(dirstream);
		windows_errno(error);
		return 0;
	}

	if (windows_first_stream(dirstream) != 0) {
		CloseHandle(dirstream->h);
		free(dirstream->buffer);
		free(dirstream);
		return 0;
	}

	return dirstream;
}

static struct windows_dirent* windows_readdir_stream(windows_dir* dirstream)
{
	if (dirstream->state == DIR_STATE_EMPTY) {
		if (windows_next_stream(dirstream) != 0) {
			free(dirstream->buffer);
			free(dirstream);
			return 0;
		}
	}

	if (dirstream->state == DIR_STATE_FILLED) {
		dirstream->state = DIR_STATE_EMPTY;
		return &dirstream->entry;
	}

	/* otherwise it's the end of stream */
	assert(dirstream->state == DIR_STATE_EOF);
	errno = 0;

	return 0;
}

static int windows_closedir_stream(windows_dir* dirstream)
{
	if (dirstream->h != INVALID_HANDLE_VALUE) {
		if (!CloseHandle(dirstream->h)) {
			DWORD error = GetLastError();

			free(dirstream->buffer);
			free(dirstream);

			windows_errno(error);
			return -1;
		}
	}

	free(dirstream->buffer);
	free(dirstream);

	return 0;
}

windows_dir* windows_opendir(const char* dir)
{
	if (!is_scan_winfind)
		return windows_opendir_stream(dir);
	else
		return windows_opendir_find(dir);
}

struct windows_dirent* windows_readdir(windows_dir* dirstream)
{
	if (!is_scan_winfind)
		return windows_readdir_stream(dirstream);
	else
		return windows_readdir_find(dirstream);
}

int windows_closedir(windows_dir* dirstream)
{
	if (!is_scan_winfind)
		return windows_closedir_stream(dirstream);
	else
		return windows_closedir_find(dirstream);
}

int windows_dirent_hidden(struct dirent* dd)
{
	return dd->d_stat.st_hidden;
}

const char* windows_stat_desc(struct stat* st)
{
	return st->st_desc;
}

void windows_sleep(unsigned seconds)
{
	Sleep(seconds * 1000);
}

int windows_link(const char* existing, const char* file)
{
	wchar_t conv_buf_file[CONV_MAX];
	wchar_t conv_buf_existing[CONV_MAX];

	if (!CreateHardLinkW(convert(conv_buf_file, file), convert(conv_buf_existing, existing), 0)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

/**
 * In Windows 10 allow creation of symlink by not privileged user.
 *
 * See: Symlinks in Windows 10!
 * https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#cQG7cx48oGH86lkI.97
 * "Specify this flag to allow creation of symbolic links when the process is not elevated"
 */
#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
#endif

int windows_symlink(const char* existing, const char* file)
{
	wchar_t conv_buf_file[CONV_MAX];
	wchar_t conv_buf_existing[CONV_MAX];

	/* We must convert to the extended-length \\?\ format if the path is too long */
	/* otherwise the link creation fails. */
	/* But we don't want to always convert it, to avoid to recreate */
	/* user symlinks different than they were before */
	if (!CreateSymbolicLinkW(convert(conv_buf_file, file), convert_if_required(conv_buf_existing, existing),
		SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)
	) {
		DWORD error = GetLastError();
		if (GetLastError() != ERROR_INVALID_PARAMETER) {
			windows_errno(error);
			return -1;
		}

		/* retry without the new flag SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE */
		if (!CreateSymbolicLinkW(convert(conv_buf_file, file), convert_if_required(conv_buf_existing, existing), 0)) {
			windows_errno(GetLastError());
			return -1;
		}
	}

	return 0;
}

/* Adds missing definitions in MingW winnt.h */
#ifndef FSCTL_GET_REPARSE_POINT
#define FSCTL_GET_REPARSE_POINT 0x000900a8
#endif
#ifndef REPARSE_DATA_BUFFER_HEADER_SIZE
typedef struct _REPARSE_DATA_BUFFER {
	DWORD ReparseTag;
	WORD ReparseDataLength;
	WORD Reserved;
	_ANONYMOUS_UNION union {
		struct {
			WORD SubstituteNameOffset;
			WORD SubstituteNameLength;
			WORD PrintNameOffset;
			WORD PrintNameLength;
			ULONG Flags;
			WCHAR PathBuffer[1];
		} SymbolicLinkReparseBuffer;
		struct {
			WORD SubstituteNameOffset;
			WORD SubstituteNameLength;
			WORD PrintNameOffset;
			WORD PrintNameLength;
			WCHAR PathBuffer[1];
		} MountPointReparseBuffer;
		struct {
			BYTE DataBuffer[1];
		} GenericReparseBuffer;
	} DUMMYUNIONNAME;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
#endif

int windows_readlink(const char* file, char* buffer, size_t size)
{
	wchar_t conv_buf_file[CONV_MAX];
	char conv_buf_name[CONV_MAX];
	HANDLE h;
	const char* name;
	size_t len;
	unsigned char rdb_buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
	REPARSE_DATA_BUFFER* rdb = (REPARSE_DATA_BUFFER*)rdb_buffer;
	BOOL ret;
	DWORD n;

	/*
	 * Open the handle of the file.
	 *
	 * Use FILE_FLAG_BACKUP_SEMANTICS to open directories (it's just ignored for files).
	 * Use FILE_FLAG_OPEN_REPARSE_POINT to open symbolic links and not the their target.
	 */
	h = CreateFileW(convert(conv_buf_file, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	/* read the reparse point */
	ret = DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, 0, 0, rdb_buffer, sizeof(rdb_buffer), &n, 0);
	if (!ret) {
		windows_errno(GetLastError());
		CloseHandle(h);
		return -1;
	}

	CloseHandle(h);

	/* check if it's really a symbolic link */
	if (rdb->ReparseTag != IO_REPARSE_TAG_SYMLINK) {
		errno = EINVAL;
		return -1;
	}

	/* convert the name to UTF-8 */
	name = u16tou8ex(conv_buf_name,
		rdb->SymbolicLinkReparseBuffer.PathBuffer + rdb->SymbolicLinkReparseBuffer.PrintNameOffset,
		rdb->SymbolicLinkReparseBuffer.PrintNameLength / 2, &len);

	/* check for overflow */
	if (len > size) {
		len = size;
	}

	memcpy(buffer, name, len);

	return len;
}

int devuuid(uint64_t device, char* uuid, size_t uuid_size)
{
	/* just use the volume serial number returned in the device parameter */
	snprintf(uuid, uuid_size, "%08x", (unsigned)device);

	log_tag("uuid:windows:%u:%s:\n", (unsigned)device, uuid);

	return 0;
}

int filephy(const char* file, uint64_t size, uint64_t* physical)
{
	wchar_t conv_buf[CONV_MAX];
	HANDLE h;

	(void)size;

	/* open the handle of the file */
	h = CreateFileW(convert(conv_buf, file), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, 0, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!GetFilePhysicalOffset(h, physical)) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	CloseHandle(h);
	return 0;
}

int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space)
{
	wchar_t conv_buf[CONV_MAX];

	/* all FAT/exFAT/NTFS when managed from Windows have persistent inodes */
	if (has_persistent_inode)
		*has_persistent_inode = 1;

	/* NTFS doesn't synchronize hardlinks metadata */
	if (has_syncronized_hardlinks)
		*has_syncronized_hardlinks = 0;

	if (free_space || total_space) {
		ULARGE_INTEGER total_bytes;
		ULARGE_INTEGER total_free_bytes;
		DWORD attr;
		char dir[PATH_MAX];

		if (strlen(path) + 1 > sizeof(dir)) {
			windows_errno(ERROR_BUFFER_OVERFLOW);
			return -1;
		}

		strcpy(dir, path);

		/* get the file attributes */
		attr = GetFileAttributesW(convert(conv_buf, dir));
		if (attr == INVALID_FILE_ATTRIBUTES) {
			DWORD error = GetLastError();

			if (error != ERROR_FILE_NOT_FOUND) {
				windows_errno(error);
				return -1;
			}

			/* if it doesn't exist, we assume a file */
			/* and we check for the containing dir */
			attr = 0;
		}

		/* if it's not a directory, truncate the file name */
		if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0) {
			char* slash = strrchr(dir, '/');

			/**
			 * Cut the file name, but leave the last slash.
			 *
			 * This is done because a MSDN comment about using of UNC paths.
			 *
			 * MSDN 'GetDiskFreeSpaceEx function'
			 * http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937%28v=vs.85%29.aspx
			 * If this parameter is a UNC name, it must include a trailing backslash,
			 * for example, "\\MyServer\MyShare\".
			 */
			if (slash)
				slash[1] = 0;
		}

		/* get the free space of the directory */
		/* note that it must be a directory */
		if (!GetDiskFreeSpaceExW(convert(conv_buf, dir), 0, &total_bytes, &total_free_bytes)) {
			windows_errno(GetLastError());
			return -1;
		}

		if (total_space)
			*total_space = total_bytes.QuadPart;
		if (free_space)
			*free_space = total_free_bytes.QuadPart;
	}

	return 0;
}

/* ensure to call the real C strerror() */
#undef strerror

const char* windows_strerror(int err)
{
	/* get the normal C error from the specified err */
	char* error;
	char* previous;
	const char* str = strerror(err);
	size_t len = strlen(str);

	/* adds space for GetLastError() */
	len += 32;

	/* allocate a new one */
	error = malloc(len);
	if (!error)
		return str;
	snprintf(error, len, "%s [%d/%u]", str, err, (unsigned)GetLastError());

	/* get previous one, if any */
	previous = windows_getspecific(last_error);

	/* store in the thread local storage */
	if (windows_setspecific(last_error, error) != 0) {
		free(error);
		return str;
	}

	free(previous);
	return error;
}

ssize_t windows_read(int fd, void* buffer, size_t size)
{
	HANDLE h;
	DWORD count;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	if (!ReadFile(h, buffer, size, &count, 0)) {
		windows_errno(GetLastError());
		return -1;
	}

	return count;
}

ssize_t windows_write(int fd, const void* buffer, size_t size)
{
	HANDLE h;
	DWORD count;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	if (!WriteFile(h, buffer, size, &count, 0)) {
		windows_errno(GetLastError());
		return -1;
	}

	return count;
}

off_t windows_lseek(int fd, off_t offset, int whence)
{
	HANDLE h;
	LARGE_INTEGER pos;
	LARGE_INTEGER ret;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	/* we support only SEEK_SET */
	if (whence != SEEK_SET) {
		errno = EINVAL;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

	pos.QuadPart = offset;
	if (!SetFilePointerEx(h, pos, &ret, FILE_BEGIN)) {
		windows_errno(GetLastError());
		return -1;
	}

	return ret.QuadPart;
}

ssize_t windows_pread(int fd, void* buffer, size_t size, off_t offset)
{
	HANDLE h;
	OVERLAPPED overlapped;
	DWORD count;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

retry:
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.Offset = offset & 0xFFFFFFFF;
	overlapped.OffsetHigh = offset >> 32;

	if (!ReadFile(h, buffer, size, &count, &overlapped)) {
		DWORD err = GetLastError();
		/*
		 * If Windows is not able to allocate memory from the PagedPool for the disk cache
		 * it could return the ERROR_NO_SYSTEM_RESOURCES error.
		 * In this case, the only possibility is to retry after a wait of few milliseconds.
		 *
		 * SQL Server reports operating system error 1450 or 1452 or 665 (retries)
		 * http://blogs.msdn.com/b/psssql/archive/2008/07/10/sql-server-reports-operating-system-error-1450-or-1452-or-665-retries.aspx
		 *
		 * 03-12-09 - ERROR_NO_SYSTEM_RESOURCES
		 * http://cbloomrants.blogspot.it/2009/03/03-12-09-errornosystemresources.html
		 *
		 * From SnapRAID Discussion Forum:
		 *
		 * Error reading file
		 * https://sourceforge.net/p/snapraid/discussion/1677233/thread/6657fdbf/
		 *
		 * Unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pwrite(), retrying...
		 * https://sourceforge.net/p/snapraid/discussion/1677233/thread/a7c25ba9/
		 */
		if (err == ERROR_NO_SYSTEM_RESOURCES) {
			log_fatal("Unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pread(), retrying...\n");
			Sleep(50);
			goto retry;
		}

		windows_errno(err);
		return -1;
	}

	return count;
}

ssize_t windows_pwrite(int fd, const void* buffer, size_t size, off_t offset)
{
	HANDLE h;
	OVERLAPPED overlapped;
	DWORD count;

	if (fd == -1) {
		errno = EBADF;
		return -1;
	}

	h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) {
		errno = EBADF;
		return -1;
	}

retry:
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.Offset = offset & 0xFFFFFFFF;
	overlapped.OffsetHigh = offset >> 32;

	if (!WriteFile(h, buffer, size, &count, &overlapped)) {
		DWORD err = GetLastError();
		/* See windows_pread() for comments on this error management */
		if (err == ERROR_NO_SYSTEM_RESOURCES) {
			log_fatal("Unexpected Windows ERROR_NO_SYSTEM_RESOURCES in pwrite(), retrying...\n");
			Sleep(50);
			goto retry;
		}

		windows_errno(err);
		return -1;
	}

	return count;
}

size_t windows_direct_size(void)
{
	SYSTEM_INFO si;

	GetSystemInfo(&si);

	/*
	 * MSDN 'File Buffering'
	 * https://msdn.microsoft.com/en-us/library/windows/desktop/cc644950%28v=vs.85%29.aspx
	 *
	 * "Therefore, in most situations, page-aligned memory will also be sector-aligned,"
	 * "because the case where the sector size is larger than the page size is rare."
	 */
	return si.dwPageSize;
}

uint64_t tick(void)
{
	LARGE_INTEGER t;
	uint64_t r;

	/*
	 * Ensure to return a strict monotone tick counter.
	 *
	 * We had reports of invalid stats due faulty High Precision Event Timer.
	 * See: https://sourceforge.net/p/snapraid/discussion/1677233/thread/a2122fd6/
	 */
	windows_mutex_lock(&tick_lock);

	/*
	 * MSDN 'QueryPerformanceCounter'
	 * "On systems that run Windows XP or later, the function"
	 * "will always succeed and will thus never return zero."
	 */
	r = 0;
	if (QueryPerformanceCounter(&t))
		r = t.QuadPart;

	if (r < tick_last)
		r = tick_last;
	tick_last = r;

	windows_mutex_unlock(&tick_lock);

	return r;
}

uint64_t tick_ms(void)
{
	/* GetTickCount64() isn't supported in Windows XP */
	if (ptr_GetTickCount64 != 0)
		return ptr_GetTickCount64();

	return GetTickCount();
}

int randomize(void* void_ptr, size_t size)
{
	size_t i;
	unsigned char* ptr = void_ptr;

	/* try RtlGenRandom */
	if (ptr_RtlGenRandom != 0 && ptr_RtlGenRandom(ptr, size) != 0)
		return 0;

	/* try rand_s */
	for (i = 0; i < size; ++i) {
		unsigned v = 0;

		if (rand_s(&v) != 0)
			break;

		ptr[i] = v;
	}
	if (i == size)
		return 0;

	/* fallback to standard rand */
	for (i = 0; i < size; ++i)
		ptr[i] = rand();

	return 0;
}

/**
 * Get the device file from a path inside the device.
 */
static int devresolve(const char* mount, char* file, size_t file_size, char* wfile, size_t wfile_size)
{
	wchar_t conv_buf_mount[CONV_MAX];
	char conv_buf_volume_guid[CONV_MAX];
	WCHAR volume_mount[MAX_PATH];
	WCHAR volume_guid[MAX_PATH];
	DWORD i;
	char* p;

	/* get the volume mount point from the disk path */
	if (!GetVolumePathNameW(convert(conv_buf_mount, mount), volume_mount, sizeof(volume_mount) / sizeof(WCHAR))) {
		windows_errno(GetLastError());
		return -1;
	}

	/* get the volume GUID path from the mount point */
	if (!GetVolumeNameForVolumeMountPointW(volume_mount, volume_guid, sizeof(volume_guid) / sizeof(WCHAR))) {
		windows_errno(GetLastError());
		return -1;
	}

	/* remove the final slash, otherwise CreateFile() opens the file-system */
	/* and not the volume */
	i = 0;
	while (volume_guid[i] != 0)
		++i;
	if (i != 0 && volume_guid[i - 1] == '\\')
		volume_guid[i - 1] = 0;

	pathcpy(wfile, wfile_size, u16tou8(conv_buf_volume_guid, volume_guid));

	/* get the GUID start { */
	p = strchr(wfile, '{');
	if (!p)
		p = wfile;
	else
		++p;

	pathprint(file, file_size, "/dev/vol%s", p);

	/* cut GUID end } */
	p = strrchr(file, '}');
	if (p)
		*p = 0;

	return 0;
}

/**
 * Read a device tree filling the specified list of disk_t entries.
 */
static int devtree(const char* name, const char* custom, const char* wfile, devinfo_t* parent, tommy_list* list)
{
	wchar_t conv_buf[CONV_MAX];
	HANDLE h;
	unsigned char vde_buffer[sizeof(VOLUME_DISK_EXTENTS)];
	VOLUME_DISK_EXTENTS* vde = (VOLUME_DISK_EXTENTS*)&vde_buffer;
	unsigned vde_size = sizeof(vde_buffer);
	void* vde_alloc = 0;
	BOOL ret;
	DWORD n;
	DWORD i;

	/* open the volume */
	h = CreateFileW(convert(conv_buf, wfile), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	if (h == INVALID_HANDLE_VALUE) {
		windows_errno(GetLastError());
		return -1;
	}

	/* get the physical extents of the volume */
	ret = DeviceIoControl(h, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, 0, 0, vde, vde_size, &n, 0);
	if (!ret) {
		DWORD error = GetLastError();
		if (error != ERROR_MORE_DATA) {
			CloseHandle(h);
			windows_errno(error);
		}

		/* more than one extends, allocate more space */
		vde_size = sizeof(VOLUME_DISK_EXTENTS) + vde->NumberOfDiskExtents * sizeof(DISK_EXTENT);
		vde_alloc = malloc_nofail(vde_size);
		vde = vde_alloc;

		/* retry with more space */
		ret = DeviceIoControl(h, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, 0, 0, vde, vde_size, &n, 0);
	}
	if (!ret) {
		DWORD error = GetLastError();
		CloseHandle(h);
		windows_errno(error);
		return -1;
	}

	for (i = 0; i < vde->NumberOfDiskExtents; ++i) {
		devinfo_t* devinfo;

		devinfo = calloc_nofail(1, sizeof(devinfo_t));

		pathcpy(devinfo->name, sizeof(devinfo->name), name);
		pathcpy(devinfo->smartctl, sizeof(devinfo->smartctl), custom);
		devinfo->device = vde->Extents[i].DiskNumber;
		pathprint(devinfo->file, sizeof(devinfo->file), "/dev/pd%" PRIu64, devinfo->device);
		pathprint(devinfo->wfile, sizeof(devinfo->wfile), "\\\\.\\PhysicalDrive%" PRIu64, devinfo->device);
		devinfo->parent = parent;

		/* insert in the list */
		tommy_list_insert_tail(list, &devinfo->node, devinfo);
	}

	if (!CloseHandle(h)) {
		windows_errno(GetLastError());
		return -1;
	}

	free(vde_alloc);

	return 0;
}

/**
 * Read smartctl --scan from a stream.
 * Return 0 on success.
 */
static int smartctl_scan(FILE* f, tommy_list* list)
{
	while (1) {
		char buf[256];
		char* s;

		s = fgets(buf, sizeof(buf), f);
		if (s == 0)
			break;

		/* remove extraneous chars */
		s = strpolish(buf);

		log_tag("smartctl:scan::text: %s\n", s);

		if (*s == '/') {
			char* sep = strchr(s, ' ');
			if (sep) {
				tommy_node* i;
				const char* number;
				uint64_t device;

				/* clear everything after the first space */
				*sep = 0;

				/* get the device number from the device file */
				/* note that this is Windows specific */
				/* for the format /dev/pdX of smartmontools */
				number = s;
				while (*number != 0 && !isdigit(*number))
					++number;
				device = atoi(number);

				/* check if already present */
				/* comparing the device file */
				for (i = tommy_list_head(list); i != 0; i = i->next) {
					devinfo_t* devinfo = i->data;
					if (devinfo->device == device)
						break;
				}

				/* if not found */
				if (i == 0) {
					devinfo_t* devinfo;

					devinfo = calloc_nofail(1, sizeof(devinfo_t));
					devinfo->device = device;
					pathprint(devinfo->file, sizeof(devinfo->file), "/dev/pd%" PRIu64, devinfo->device);
					pathprint(devinfo->wfile, sizeof(devinfo->wfile), "\\\\.\\PhysicalDrive%" PRIu64, devinfo->device);

					/* insert in the list */
					tommy_list_insert_tail(list, &devinfo->node, devinfo);
				}
			}
		}
	}

	return 0;
}

/**
 * Scan all the devices.
 */
static int devscan(tommy_list* list)
{
	char conv_buf[CONV_MAX];
	WCHAR cmd[MAX_PATH + 128];
	FILE* f;
	int ret;

	snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" --scan-open -d pd", exedir);

	log_tag("smartctl:scan::run: %s\n", u16tou8(conv_buf, cmd));

	f = _wpopen(cmd, L"rt");
	if (!f) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from popen).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}

	if (smartctl_scan(f, list) != 0) {
		/* LCOV_EXCL_START */
		pclose(f);
		return -1;
		/* LCOV_EXCL_STOP */
	}

	ret = pclose(f);

	log_tag("smartctl:scan::ret: %x\n", ret);

	if (ret == -1) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from pclose).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}
	if (ret != 0) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' with return code %xh.\n", u16tou8(conv_buf, cmd), ret);
		return -1;
		/* LCOV_EXCL_STOP */
	}

	return 0;
}

/**
 * Get SMART attributes.
 */
static int devsmart(uint64_t device, const char* name, const char* custom, uint64_t* smart, char* serial, char* vendor, char* model)
{
	char conv_buf[CONV_MAX];
	WCHAR cmd[MAX_PATH + 128];
	char file[128];
	FILE* f;
	int ret;
	int count;

	snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);

	/* if there is a custom command */
	if (custom[0]) {
		char option[128];
		snprintf(option, sizeof(option), custom, file);
		snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -a %s", exedir, option);
	} else {
		snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -a %s", exedir, file);
	}

	count = 0;

retry:
	log_tag("smartctl:%s:%s:run: %s\n", file, name, u16tou8(conv_buf, cmd));

	f = _wpopen(cmd, L"rt");
	if (!f) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from popen).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}

	if (smartctl_attribute(f, file, name, smart, serial, vendor, model) != 0) {
		/* LCOV_EXCL_START */
		pclose(f);
		return -1;
		/* LCOV_EXCL_STOP */
	}

	ret = pclose(f);

	log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);

	if (ret == -1) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from pclose).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}

	/* if first try without custom command */
	if (count == 0 && custom[0] == 0) {
		/*
		 * Handle some common cases in Windows.
		 *
		 * Sometimes the "type" autodetection is wrong, and the command fails at identification
		 * stage, returning with error 2, or even with error 0, and with no info at all.
		 * We detect this condition checking the PowerOnHours, Size and RotationRate attributes.
		 *
		 * In such conditions we retry using the "sat" type, that often allows to proceed.
		 *
		 * Note that getting error 4 is instead very common, even with full info gathering.
		 */
		if ((ret == 0 || ret == 2)
			&& smart[9] == SMART_UNASSIGNED
			&& smart[SMART_SIZE] == SMART_UNASSIGNED
			&& smart[SMART_ROTATION_RATE] == SMART_UNASSIGNED
		) {
			/* retry using the "sat" type */
			snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -a -d sat %s", exedir, file);

			++count;
			goto retry;
		}
	}

	/* store the smartctl return value */
	smart[SMART_FLAGS] = ret;

	return 0;
}

/**
 * Spin down a specific device.
 */
static int devdown(uint64_t device, const char* name, const char* custom)
{
	char conv_buf[CONV_MAX];
	WCHAR cmd[MAX_PATH + 128];
	char file[128];
	FILE* f;
	int ret;
	int count;

	snprintf(file, sizeof(file), "/dev/pd%" PRIu64, device);

	/* if there is a custom command */
	if (custom[0]) {
		char option[128];
		snprintf(option, sizeof(option), custom, file);
		snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -s standby,now %s", exedir, option);
	} else {
		snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -s standby,now %s", exedir, file);
	}

	count = 0;

retry:
	log_tag("smartctl:%s:%s:run: %s\n", file, name, u16tou8(conv_buf, cmd));

	f = _wpopen(cmd, L"rt");
	if (!f) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from popen).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}

	if (smartctl_flush(f, file, name) != 0) {
		/* LCOV_EXCL_START */
		pclose(f);
		return -1;
		/* LCOV_EXCL_STOP */
	}

	ret = pclose(f);

	log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);

	if (ret == -1) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' (from pclose).\n", u16tou8(conv_buf, cmd));
		return -1;
		/* LCOV_EXCL_STOP */
	}

	/* if first try without custom command */
	if (count == 0 && custom[0] == 0) {
		/*
		 * Handle some common cases in Windows.
		 *
		 * Sometimes the "type" autodetection is wrong, and the command fails at identification
		 * stage, returning with error 2.
		 *
		 * In such conditions we retry using the "sat" type, that often allows to proceed.
		 */
		if (ret == 2) {
			/* retry using the "sat" type */
			snwprintf(cmd, sizeof(cmd), L"\"%lssmartctl.exe\" -s standby,now -d sat %s", exedir, file);

			++count;
			goto retry;
		}
	}

	if (ret != 0) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to run '%s' with return code %xh.\n", u16tou8(conv_buf, cmd), ret);
		return -1;
		/* LCOV_EXCL_STOP */
	}

	return 0;
}

/**
 * Spin up a device.
 *
 * There isn't a defined way to spin up a device,
 * so we just do a generic write.
 */
static int devup(const char* mount)
{
	wchar_t conv_buf[CONV_MAX];
	int f;
	char path[PATH_MAX];

	/* add a temporary name used for writing */
	pathprint(path, sizeof(path), "%s.snapraid-spinup.tmp", mount);

	/* create a temporary file, automatically deleted on close */
	f = _wopen(convert(conv_buf, path), _O_CREAT | _O_TEMPORARY | _O_RDWR, _S_IREAD | _S_IWRITE);
	if (f != -1)
		close(f);

	return 0;
}

/**
 * Thread for spinning up.
 *
 * Note that filling up the devinfo object is done inside this thread,
 * to avoid to block the main thread if the device need to be spin up
 * to handle stat/resolve requests.
 */
static void* thread_spinup(void* arg)
{
	devinfo_t* devinfo = arg;
	struct stat st;
	uint64_t start;

	start = tick_ms();

	/* uses lstat_sync() that maps to CreateFile */
	/* we cannot use FindFirstFile because it doesn't allow to open the root dir */
	if (lstat_sync(devinfo->mount, &st, 0) != 0) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to stat path '%s'. %s.\n", devinfo->mount, strerror(errno));
		return (void*)-1;
		/* LCOV_EXCL_STOP */
	}

	/* set the device number */
	devinfo->device = st.st_dev;

	if (devresolve(devinfo->mount, devinfo->file, sizeof(devinfo->file), devinfo->wfile, sizeof(devinfo->wfile)) != 0) {
		/* LCOV_EXCL_START */
		log_fatal("Failed to resolve path '%s'.\n", devinfo->mount);
		return (void*)-1;
		/* LCOV_EXCL_STOP */
	}

	if (devup(devinfo->mount) != 0) {
		/* LCOV_EXCL_START */
		return (void*)-1;
		/* LCOV_EXCL_STOP */
	}

	msg_status("Spunup device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, tick_ms() - start);

	return 0;
}

/**
 * Thread for spinning down.
 */
static void* thread_spindown(void* arg)
{
	devinfo_t* devinfo = arg;
	uint64_t start;

	start = tick_ms();

	if (devdown(devinfo->device, devinfo->name, devinfo->smartctl) != 0) {
		/* LCOV_EXCL_START */
		return (void*)-1;
		/* LCOV_EXCL_STOP */
	}

	msg_status("Spundown device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, tick_ms() - start);

	return 0;
}

/**
 * Thread for getting smart info.
 */
static void* thread_smart(void* arg)
{
	devinfo_t* devinfo = arg;

	if (devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smart, devinfo->smart_serial, devinfo->smart_vendor, devinfo->smart_model) != 0) {
		/* LCOV_EXCL_START */
		return (void*)-1;
		/* LCOV_EXCL_STOP */
	}

	return 0;
}

static int device_thread(tommy_list* list, void* (*func)(void* arg))
{
	int fail = 0;
	tommy_node* i;

	/* starts all threads */
	for (i = tommy_list_head(list); i != 0; i = i->next) {
		devinfo_t* devinfo = i->data;

		thread_create(&devinfo->thread, func, devinfo);
	}

	/* joins all threads */
	for (i = tommy_list_head(list); i != 0; i = i->next) {
		devinfo_t* devinfo = i->data;
		void* retval;

		thread_join(devinfo->thread, &retval);

		if (retval != 0)
			++fail;
	}

	if (fail != 0) {
		/* LCOV_EXCL_START */
		return -1;
		/* LCOV_EXCL_STOP */
	}

	return 0;
}

int devquery(tommy_list* high, tommy_list* low, int operation, int others)
{
	tommy_node* i;
	void* (*func)(void* arg) = 0;

	if (operation != DEVICE_UP) {
		/* for each device */
		for (i = tommy_list_head(high); i != 0; i = i->next) {
			devinfo_t* devinfo = i->data;

			if (devresolve(devinfo->mount, devinfo->file, sizeof(devinfo->file), devinfo->wfile, sizeof(devinfo->wfile)) != 0) {
				/* LCOV_EXCL_START */
				log_fatal("Failed to resolve path '%s'.\n", devinfo->mount);
				return -1;
				/* LCOV_EXCL_STOP */
			}

			/* expand the tree of devices */
			if (devtree(devinfo->name, devinfo->smartctl, devinfo->wfile, devinfo, low) != 0) {
				/* LCOV_EXCL_START */
				log_fatal("Failed to expand device '%s'.\n", devinfo->file);
				return -1;
				/* LCOV_EXCL_STOP */
			}
		}
	}

	if (operation == DEVICE_UP) {
		/* duplicate the high */
		for (i = tommy_list_head(high); i != 0; i = i->next) {
			devinfo_t* devinfo = i->data;
			devinfo_t* entry;

			entry = calloc_nofail(1, sizeof(devinfo_t));

			entry->device = devinfo->device;
			pathcpy(entry->name, sizeof(entry->name), devinfo->name);
			pathcpy(entry->mount, sizeof(entry->mount), devinfo->mount);

			/* insert in the high */
			tommy_list_insert_tail(low, &entry->node, entry);
		}
	}

	/* add other devices */
	if (others) {
		if (devscan(low) != 0) {
			/* LCOV_EXCL_START */
			log_fatal("Failed to list other devices.\n");
			return -1;
			/* LCOV_EXCL_STOP */
		}
	}

	switch (operation) {
	case DEVICE_UP : func = thread_spinup; break;
	case DEVICE_DOWN : func = thread_spindown; break;
	case DEVICE_SMART : func = thread_smart; break;
	}

	if (!func)
		return 0;

	return device_thread(low, func);
}

/****************************************************************************/
/* pthread like interface */

int windows_mutex_init(windows_mutex_t* mutex, void* attr)
{
	(void)attr;

	InitializeCriticalSection(mutex);

	return 0;
}

int windows_mutex_destroy(windows_mutex_t* mutex)
{
	DeleteCriticalSection(mutex);

	return 0;
}

int windows_mutex_lock(windows_mutex_t* mutex)
{
	EnterCriticalSection(mutex);

	return 0;
}

int windows_mutex_unlock(windows_mutex_t* mutex)
{
	LeaveCriticalSection(mutex);

	return 0;
}

int windows_cond_init(windows_cond_t* cond, void* attr)
{
	(void)attr;

	InitializeConditionVariable(cond);

	return 0;
}

int windows_cond_destroy(windows_cond_t* cond)
{
	/* note that in Windows there is no DeleteConditionVariable() to call */
	(void)cond;

	return 0;
}

int windows_cond_signal(windows_cond_t* cond)
{
	WakeConditionVariable(cond);

	return 0;
}

int windows_cond_broadcast(windows_cond_t* cond)
{
	WakeAllConditionVariable(cond);

	return 0;
}

int windows_cond_wait(windows_cond_t* cond, windows_mutex_t* mutex)
{
	if (!SleepConditionVariableCS(cond, mutex, INFINITE))
		return -1;

	return 0;
}

struct windows_key_context {
	void (* func)(void *);
	DWORD key;
	tommy_node node;
};

/* list of all keys with destructor */
static tommy_list windows_key_list = { 0 };

int windows_key_create(windows_key_t* key, void(* destructor)(void*))
{
	struct windows_key_context* context;

	context = malloc(sizeof(struct windows_key_context));
	if (!context)
		return -1;

	context->func = destructor;
	context->key = TlsAlloc();
	if (context->key == 0xFFFFFFFF) {
		windows_errno(GetLastError());
		free(context);
		return -1;
	}

	/* insert in the list of destructors */
	if (context->func)
		tommy_list_insert_tail(&windows_key_list, &context->node, context);

	*key = context;

	return 0;
}

int windows_key_delete(windows_key_t key)
{
	struct windows_key_context* context = key;

	/* remove from the list of destructors */
	if (context->func)
		tommy_list_remove_existing(&windows_key_list, &context->node);

	TlsFree(context->key);

	free(context);

	return 0;
}

void* windows_getspecific(windows_key_t key)
{
	struct windows_key_context* context = key;

	return TlsGetValue(context->key);
}

int windows_setspecific(windows_key_t key, void* value)
{
	struct windows_key_context* context = key;

	if (!TlsSetValue(context->key, value)) {
		windows_errno(GetLastError());
		return -1;
	}

	return 0;
}

struct windows_thread_context {
	HANDLE h;
	unsigned id;
	void* (* func)(void *);
	void* arg;
	void* ret;
};

/* forwarder to change the function declaration */
static unsigned __stdcall windows_thread_func(void* arg)
{
	struct windows_thread_context* context = arg;
	tommy_node* i;

	context->ret = context->func(context->arg);

	/* call the destructor of all the keys */
	i = tommy_list_head(&windows_key_list);
	while (i) {
		struct windows_key_context* key = i->data;
		if (key->func) {
			void* value = windows_getspecific(key);
			if (value)
				key->func(value);
		}
		i = i->next;
	}

	return 0;
}

int windows_create(thread_id_t* thread, void* attr, void* (* func)(void *), void* arg)
{
	struct windows_thread_context* context;

	(void)attr;

	context = malloc(sizeof(struct windows_thread_context));
	if (!context)
		return -1;

	context->func = func;
	context->arg = arg;
	context->ret = 0;
	context->h = (void*)_beginthreadex(0, 0, windows_thread_func, context, 0, &context->id);

	if (context->h == 0) {
		free(context);
		return -1;
	}

	*thread = context;

	return 0;
}

int windows_join(thread_id_t thread, void** retval)
{
	struct windows_thread_context* context = thread;

	if (WaitForSingleObject(context->h, INFINITE) != WAIT_OBJECT_0) {
		windows_errno(GetLastError());
		return -1;
	}

	if (!CloseHandle(context->h)) {
		windows_errno(GetLastError());
		return -1;
	}

	*retval = context->ret;

	free(context);

	return 0;
}

#endif

