/**
 * @file winposix.c
 *
 * @copyright 2015-2026 Bill Zissimopoulos
 */
/*
 * This file is part of WinFsp.
 *
 * You can redistribute it and/or modify it under the terms of the GNU
 * General Public License version 3 as published by the Free Software
 * Foundation.
 *
 * Licensees holding a valid commercial license may use this software
 * in accordance with the commercial license agreement provided in
 * conjunction with the software.  The terms and conditions of any such
 * commercial license agreement shall govern, supersede, and render
 * ineffective any application of the GPLv3 license to this software,
 * notwithstanding of any reference thereto in the software or
 * associated repository.
 */

/*
 * This is a very simple Windows POSIX layer. It handles all the POSIX
 * file API's required to implement passthrough-fuse in POSIX, however
 * the API handling is rather unsophisticated.
 *
 * Ways to improve it: use the FspPosix* API's to properly handle
 * file names and security.
 */

#include <winfsp/winfsp.h>
#include <fcntl.h>
#include <fuse.h>
#include "winposix.h"

#pragma comment(lib, "ntdll.lib")

typedef struct _FILE_GET_EA_INFORMATION
{
    ULONG NextEntryOffset;
    UCHAR EaNameLength;
    CHAR EaName[1];
} FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;

NTSYSAPI NTSTATUS NTAPI NtQueryEaFile(
    IN HANDLE               FileHandle,
    OUT PIO_STATUS_BLOCK    IoStatusBlock,
    OUT PVOID               Buffer,
    IN ULONG                Length,
    IN BOOLEAN              ReturnSingleEntry,
    IN PVOID                EaList OPTIONAL,
    IN ULONG                EaListLength,
    IN PULONG               EaIndex OPTIONAL,
    IN BOOLEAN              RestartScan);
NTSYSAPI NTSTATUS NTAPI NtSetEaFile(
    IN HANDLE               FileHandle,
    OUT PIO_STATUS_BLOCK    IoStatusBlock,
    IN PVOID                EaBuffer,
    IN ULONG                EaBufferSize);
#define NEXT_EA(Ea, EaEnd)              \
    (0 != (Ea)->NextEntryOffset ? (PVOID)((PUINT8)(Ea) + (Ea)->NextEntryOffset) : (EaEnd))

struct _DIR
{
    HANDLE h, fh;
    struct dirent de;
    char path[];
};

#if defined(FSP_FUSE_USE_STAT_EX)
static inline uint32_t MapFileAttributesToFlags(UINT32 FileAttributes)
{
    uint32_t flags = 0;

    if (FileAttributes & FILE_ATTRIBUTE_READONLY)
        flags |= FSP_FUSE_UF_READONLY;
    if (FileAttributes & FILE_ATTRIBUTE_HIDDEN)
        flags |= FSP_FUSE_UF_HIDDEN;
    if (FileAttributes & FILE_ATTRIBUTE_SYSTEM)
        flags |= FSP_FUSE_UF_SYSTEM;
    if (FileAttributes & FILE_ATTRIBUTE_ARCHIVE)
        flags |= FSP_FUSE_UF_ARCHIVE;

    return flags;
}

static inline UINT32 MapFlagsToFileAttributes(uint32_t flags)
{
    UINT32 FileAttributes = 0;

    if (flags & FSP_FUSE_UF_READONLY)
        FileAttributes |= FILE_ATTRIBUTE_READONLY;
    if (flags & FSP_FUSE_UF_HIDDEN)
        FileAttributes |= FILE_ATTRIBUTE_HIDDEN;
    if (flags & FSP_FUSE_UF_SYSTEM)
        FileAttributes |= FILE_ATTRIBUTE_SYSTEM;
    if (flags & FSP_FUSE_UF_ARCHIVE)
        FileAttributes |= FILE_ATTRIBUTE_ARCHIVE;

    return FileAttributes;
}
#endif

static int maperror(int winerrno);

static inline void *error0(void)
{
    errno = maperror(GetLastError());
    return 0;
}

static inline int error(void)
{
    errno = maperror(GetLastError());
    return -1;
}

char *realpath(const char *path, char *resolved)
{
    char *result;

    if (0 == resolved)
    {
        result = malloc(PATH_MAX); /* sets errno */
        if (0 == result)
            return 0;
    }
    else
        result = resolved;

    int err = 0;
    WCHAR PathBuf[PATH_MAX];
    WCHAR ResultBuf[PATH_MAX];
    PWSTR ResultBufBgn = &ResultBuf[6];
    if (0 == MultiByteToWideChar(CP_UTF8, 0, path, -1, PathBuf, PATH_MAX))
        err = GetLastError();
    else
    {
        DWORD len = GetFullPathNameW(PathBuf, PATH_MAX - 6, ResultBufBgn, 0);
        if (0 == len)
            err = GetLastError();
        else if (PATH_MAX - 6 < len)
            err = ERROR_INVALID_PARAMETER;
        else
        {
            if (0 == WideCharToMultiByte(CP_UTF8, 0, ResultBufBgn, -1, result, PATH_MAX, 0, 0))
                err = GetLastError();
            else
            {
                if (L'\\' == ResultBufBgn[0] && L'\\' == ResultBufBgn[1])
                {
                    ResultBufBgn = ResultBuf;
                    ResultBufBgn[0] = L'\\';
                    ResultBufBgn[1] = L'\\';
                    ResultBufBgn[2] = L'?';
                    ResultBufBgn[3] = L'\\';
                    ResultBufBgn[4] = L'U';
                    ResultBufBgn[5] = L'N';
                    ResultBufBgn[6] = L'C';
                }
                else
                {
                    ResultBufBgn = &ResultBuf[2];
                    ResultBufBgn[0] = L'\\';
                    ResultBufBgn[1] = L'\\';
                    ResultBufBgn[2] = L'?';
                    ResultBufBgn[3] = L'\\';
                }
            }
        }
    }

    if (0 == err)
    {
        HANDLE h = CreateFileW(ResultBufBgn,
            FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            0,
            OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
        if (INVALID_HANDLE_VALUE != h)
            CloseHandle(h);
        else
            err = GetLastError();
    }

    if (0 != err)
    {
        if (result != resolved)
            free(result);

        errno = maperror(err);
        result = 0;
    }
    return result;
}

static int uncpath(const char *path, WCHAR *buf, int nchar)
{
    PWSTR BufP = 0;
    if ('\\' == path[0] && '\\' == path[1])
    {
        if (8 < nchar &&
            0 < MultiByteToWideChar(CP_UTF8, 0, &path[2], -1, &buf[8], nchar - 8))
        {
            BufP = &buf[8];
            buf[0] = L'\\';
            buf[1] = L'\\';
            buf[2] = L'?';
            buf[3] = L'\\';
            buf[4] = L'U';
            buf[5] = L'N';
            buf[6] = L'C';
            buf[7] = L'\\';
        }
    }
    else
    {
        if (4 < nchar &&
            0 < MultiByteToWideChar(CP_UTF8, 0, path, -1, &buf[4], nchar - 4))
        {
            BufP = &buf[4];
            buf[0] = L'\\';
            buf[1] = L'\\';
            buf[2] = L'?';
            buf[3] = L'\\';
        }
    }

    if (0 == BufP)
    {
        if (0 < nchar)
            buf[0] = 0;
        return 0;
    }

    PWSTR P = BufP;
    while (*BufP)
    {
        if (L'/' == *BufP || L'\\' == *BufP)
        {
            while (L'/' == BufP[1] || L'\\' == BufP[1])
                BufP++;
        }
        if (L'/' == *BufP)
            *P = L'\\';
        else if (P != BufP)
            *P = *BufP;
        BufP++;
        P++;
    }
    if (P != BufP)
        *P = 0;
    return 1;
}

int statvfs(const char *path, struct fuse_statvfs *stbuf)
{
    WCHAR root[PATH_MAX];
    DWORD
        VolumeSerialNumber,
        MaxComponentLength,
        SectorsPerCluster,
        BytesPerSector,
        NumberOfFreeClusters,
        TotalNumberOfClusters;

    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    if (!GetVolumePathNameW(PathBuf, root, PATH_MAX) ||
        !GetVolumeInformationW(root, 0, 0, &VolumeSerialNumber, &MaxComponentLength, 0, 0, 0) ||
        !GetDiskFreeSpaceW(root, &SectorsPerCluster, &BytesPerSector,
            &NumberOfFreeClusters, &TotalNumberOfClusters))
    {
        return error();
    }

    memset(stbuf, 0, sizeof *stbuf);
    stbuf->f_bsize = SectorsPerCluster * BytesPerSector;
    stbuf->f_frsize = SectorsPerCluster * BytesPerSector;
    stbuf->f_blocks = TotalNumberOfClusters;
    stbuf->f_bfree = NumberOfFreeClusters;
    stbuf->f_bavail = TotalNumberOfClusters;
    stbuf->f_fsid = VolumeSerialNumber;
    stbuf->f_namemax = MaxComponentLength;

    return 0;
}

int open(const char *path, int oflag, ...)
{
    static DWORD da[] = { GENERIC_READ, GENERIC_WRITE, GENERIC_READ | GENERIC_WRITE, 0 };
    static DWORD cd[] = { OPEN_EXISTING, OPEN_ALWAYS, TRUNCATE_EXISTING, CREATE_ALWAYS };
    DWORD DesiredAccess = 0 == (oflag & _O_APPEND) ?
        da[oflag & (_O_RDONLY | _O_WRONLY | _O_RDWR)] :
        (da[oflag & (_O_RDONLY | _O_WRONLY | _O_RDWR)] & ~FILE_WRITE_DATA) | FILE_APPEND_DATA;
    DWORD CreationDisposition = (_O_CREAT | _O_EXCL) == (oflag & (_O_CREAT | _O_EXCL)) ?
        CREATE_NEW :
        cd[(oflag & (_O_CREAT | _O_TRUNC)) >> 8];

    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        DesiredAccess, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0/* default security */,
        CreationDisposition, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, 0);

    if (INVALID_HANDLE_VALUE == h)
        return error();

    return (int)(intptr_t)h;
}

int fgetpath(int fd, char *buf, size_t size)
{
    HANDLE h = (HANDLE)(intptr_t)fd;
    WCHAR FileName[FSP_FSCTL_TRANSACT_PATH_SIZEMAX / sizeof(WCHAR)];
    DWORD FileNameLength;

    FileNameLength = GetFinalPathNameByHandleW(h, FileName, sizeof FileName / sizeof(WCHAR),
        VOLUME_NAME_NONE | FILE_NAME_NORMALIZED);
    if (0 == FileNameLength)
        return error();

    FspPosixEncodeWindowsPath(FileName, FileNameLength);
    size = WideCharToMultiByte(CP_UTF8, 0, FileName, FileNameLength, buf, (int)(size - 1), 0, 0);
    if (0 == size)
        return error();
    buf[size] = '\0';

    return 0;
}

int fstat(int fd, struct fuse_stat *stbuf)
{
    HANDLE h = (HANDLE)(intptr_t)fd;
    BY_HANDLE_FILE_INFORMATION FileInfo;

    if (!GetFileInformationByHandle(h, &FileInfo))
        return error();

    memset(stbuf, 0, sizeof *stbuf);
    stbuf->st_mode = 0777 |
        ((FileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? 0040000/* S_IFDIR */ : 0);
    stbuf->st_nlink = 1;
    stbuf->st_size = ((UINT64)FileInfo.nFileSizeHigh << 32) | ((UINT64)FileInfo.nFileSizeLow);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FileInfo.ftCreationTime, (void *)&stbuf->st_birthtim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FileInfo.ftLastAccessTime, (void *)&stbuf->st_atim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FileInfo.ftLastWriteTime, (void *)&stbuf->st_mtim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FileInfo.ftLastWriteTime, (void *)&stbuf->st_ctim);
#if defined(FSP_FUSE_USE_STAT_EX)
    stbuf->st_flags = MapFileAttributesToFlags(FileInfo.dwFileAttributes);
#endif

    return 0;
}

int ftruncate(int fd, fuse_off_t size)
{
    HANDLE h = (HANDLE)(intptr_t)fd;
    FILE_END_OF_FILE_INFO EndOfFileInfo;

    EndOfFileInfo.EndOfFile.QuadPart = size;

    if (!SetFileInformationByHandle(h, FileEndOfFileInfo, &EndOfFileInfo, sizeof EndOfFileInfo))
        return error();

    return 0;
}

int pread(int fd, void *buf, size_t nbyte, fuse_off_t offset)
{
    HANDLE h = (HANDLE)(intptr_t)fd;
    OVERLAPPED Overlapped = { 0 };
    DWORD BytesTransferred;

    Overlapped.Offset = (DWORD)offset;
    Overlapped.OffsetHigh = (DWORD)(offset >> 32);

    if (!ReadFile(h, buf, (DWORD)nbyte, &BytesTransferred, &Overlapped))
    {
        if (ERROR_HANDLE_EOF == GetLastError())
            return 0;
        return error();
    }

    return BytesTransferred;
}

int pwrite(int fd, const void *buf, size_t nbyte, fuse_off_t offset)
{
    HANDLE h = (HANDLE)(intptr_t)fd;
    OVERLAPPED Overlapped = { 0 };
    DWORD BytesTransferred;

    Overlapped.Offset = (DWORD)offset;
    Overlapped.OffsetHigh = (DWORD)(offset >> 32);

    if (!WriteFile(h, buf, (DWORD)nbyte, &BytesTransferred, &Overlapped))
        return error();

    return BytesTransferred;
}

int fsync(int fd)
{
    HANDLE h = (HANDLE)(intptr_t)fd;

    if (!FlushFileBuffers(h))
        return error();

    return 0;
}

int close(int fd)
{
    HANDLE h = (HANDLE)(intptr_t)fd;

    if (!CloseHandle(h))
        return error();

    return 0;
}

int getpath(const char *path, char *buf, size_t size)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    int res = fgetpath((int)(intptr_t)h, buf, size);

    CloseHandle(h);

    return res;
}

int lstat(const char *path, struct fuse_stat *stbuf)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    int res = fstat((int)(intptr_t)h, stbuf);

    CloseHandle(h);

    return res;
}

int chmod(const char *path, fuse_mode_t mode)
{
    /* we do not support file security */
    return 0;
}

int lchown(const char *path, fuse_uid_t uid, fuse_gid_t gid)
{
    /* we do not support file security */
    return 0;
}

int lchflags(const char *path, uint32_t flags)
{
#if defined(FSP_FUSE_USE_STAT_EX)
    UINT32 FileAttributes = MapFlagsToFileAttributes(flags);

    if (0 == FileAttributes)
        FileAttributes = FILE_ATTRIBUTE_NORMAL;

    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    if (!SetFileAttributesW(PathBuf, FileAttributes))
        return error();
#endif

    return 0;
}

int truncate(const char *path, fuse_off_t size)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_WRITE_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    int res = ftruncate((int)(intptr_t)h, size);

    CloseHandle(h);

    return res;
}

int utime(const char *path, const struct fuse_utimbuf *timbuf)
{
    if (0 == timbuf)
        return utimensat(AT_FDCWD, path, 0, AT_SYMLINK_NOFOLLOW);
    else
    {
        struct fuse_timespec times[2];
        times[0].tv_sec = timbuf->actime;
        times[0].tv_nsec = 0;
        times[1].tv_sec = timbuf->modtime;
        times[1].tv_nsec = 0;
        return utimensat(AT_FDCWD, path, times, AT_SYMLINK_NOFOLLOW);
    }
}

int utimensat(int dirfd, const char *path, const struct fuse_timespec times[2], int flag)
{
    /* ignore dirfd and assume that it is always AT_FDCWD */
    /* ignore flag and assume that it is always AT_SYMLINK_NOFOLLOW */

    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    UINT64 LastAccessTime, LastWriteTime;
    if (0 == times)
    {
        FILETIME FileTime;
        GetSystemTimeAsFileTime(&FileTime);
        LastAccessTime = LastWriteTime = *(PUINT64)&FileTime;
    }
    else
    {
        FspPosixUnixTimeToFileTime((void *)&times[0], &LastAccessTime);
        FspPosixUnixTimeToFileTime((void *)&times[1], &LastWriteTime);
    }

    int res = SetFileTime(h,
        0, (PFILETIME)&LastAccessTime, (PFILETIME)&LastWriteTime) ? 0 : error();

    CloseHandle(h);

    return res;
}

int setcrtime(const char *path, const struct fuse_timespec *tv)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    UINT64 CreationTime;
    FspPosixUnixTimeToFileTime((void *)tv, &CreationTime);

    int res = SetFileTime(h,
        (PFILETIME)&CreationTime, 0, 0) ? 0 : error();

    CloseHandle(h);

    return res;
}

int unlink(const char *path)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    if (!DeleteFileW(PathBuf))
        return error();

    return 0;
}

int rename(const char *oldpath, const char *newpath)
{
    WCHAR OldPathBuf[PATH_MAX];
    WCHAR NewPathBuf[PATH_MAX];
    uncpath(oldpath, OldPathBuf, PATH_MAX);
    uncpath(newpath, NewPathBuf, PATH_MAX);
    if (!MoveFileExW(OldPathBuf, NewPathBuf, MOVEFILE_REPLACE_EXISTING))
        return error();

    return 0;
}

static int lsetea(const char *path, PFILE_FULL_EA_INFORMATION Ea, ULONG EaLength)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_WRITE_EA | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    IO_STATUS_BLOCK Iosb;
    NTSTATUS Status = NtSetEaFile(h, &Iosb, Ea, EaLength);

    CloseHandle(h);

    if (!NT_SUCCESS(Status))
        switch (Status)
        {
        case STATUS_INVALID_EA_NAME:
        case STATUS_EA_LIST_INCONSISTENT:
        case STATUS_EA_CORRUPT_ERROR:
        case STATUS_NONEXISTENT_EA_ENTRY:
        case STATUS_NO_MORE_EAS:
        case STATUS_NO_EAS_ON_FILE:
            errno = EINVAL;
            return -1;
        default:
            SetLastError(RtlNtStatusToDosError(Status));
            return error();
        }

    return 0;
}

static int lgetea(const char *path,
    PFILE_GET_EA_INFORMATION GetEa, ULONG GetEaLength,
    PFILE_FULL_EA_INFORMATION Ea, ULONG EaLength)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_READ_EA | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error();

    IO_STATUS_BLOCK Iosb;
    NTSTATUS Status = NtQueryEaFile(h, &Iosb, Ea, EaLength, FALSE, GetEa, GetEaLength, 0, TRUE);

    CloseHandle(h);

    if (!NT_SUCCESS(Status))
        switch (Status)
        {
        case STATUS_INVALID_EA_NAME:
        case STATUS_EA_LIST_INCONSISTENT:
        case STATUS_EA_CORRUPT_ERROR:
        case STATUS_NONEXISTENT_EA_ENTRY:
        case STATUS_NO_MORE_EAS:
            errno = EINVAL;
            return -1;
        case STATUS_NO_EAS_ON_FILE:
            if (0 == GetEa)
                return 0;
            else
            {
                errno = ENODATA;
                return -1;
            }
        default:
            SetLastError(RtlNtStatusToDosError(Status));
            return error();
        }
    else if (0 == GetEa &&
        (FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) > Iosb.Information || 0 == Ea->EaValueLength))
    {
        errno = ENODATA;
        return -1;
    }

    return (ULONG)Iosb.Information;
}

int lsetxattr(const char *path, const char *name, const void *value, size_t size, int flags)
{
    union
    {
        FILE_FULL_EA_INFORMATION V;
        UINT8 B[1024];
    } EaBuf;
    PFILE_FULL_EA_INFORMATION Ea = &EaBuf.V;
    ULONG EaLength;

    size_t namelen = strlen(name);
    if (254 < namelen || 0xffff < size)
    {
        errno = EINVAL;
        return -1;
    }

    EaLength = (ULONG)(FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) + namelen + 1 + size);
    if (sizeof EaBuf < EaLength)
    {
        Ea = malloc(EaLength); /* sets errno */
        if (0 == Ea)
            return -1;
    }

    memset(Ea, 0, sizeof(FILE_FULL_EA_INFORMATION));
    Ea->EaNameLength = (UCHAR)namelen;
    Ea->EaValueLength = (USHORT)size;
    memcpy(Ea->EaName, name, namelen + 1);
    memcpy(Ea->EaName + namelen + 1, value, size);

    int res = lsetea(path, Ea, EaLength); /* sets errno */

    if (&EaBuf.V != Ea)
        free(Ea);

    return res;
}

int lgetxattr(const char *path, const char *name, void *value, size_t size0)
{
    size_t size = 0 == size0 || 0xffff < size0 ? 0xffff : size0;
    union
    {
        FILE_GET_EA_INFORMATION V;
        UINT8 B[FIELD_OFFSET(FILE_GET_EA_INFORMATION, EaName) + 255];
    } GetEaBuf;
    PFILE_GET_EA_INFORMATION GetEa = &GetEaBuf.V;
    union
    {
        FILE_FULL_EA_INFORMATION V;
        UINT8 B[1024];
    } EaBuf;
    PFILE_FULL_EA_INFORMATION Ea = &EaBuf.V;
    ULONG GetEaLength, EaLength;

    size_t namelen = strlen(name);
    if (254 < namelen)
    {
        errno = EINVAL;
        return -1;
    }

    EaLength = (ULONG)(FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) + namelen + 1 + size);
    if (sizeof EaBuf < EaLength)
    {
        Ea = malloc(EaLength); /* sets errno */
        if (0 == Ea)
            return -1;
    }

    GetEaLength = (ULONG)(FIELD_OFFSET(FILE_GET_EA_INFORMATION, EaName) + namelen + 1);
    memset(GetEa, 0, sizeof(FILE_GET_EA_INFORMATION));
    GetEa->EaNameLength = (UCHAR)namelen;
    memcpy(GetEa->EaName, name, namelen + 1);

    int res = lgetea(path, GetEa, GetEaLength, Ea, EaLength);
    if (0 < res)
    {
        res = Ea->EaValueLength;
        if (0 == size0)
            ;
        else if (res <= size0)
            memcpy(value, Ea->EaName + Ea->EaNameLength + 1, res);
        else
        {
            errno = ERANGE;
            res = -1;
        }
    }
    else if (0 == res) /* should not happen! */
    {
    }

    if (&EaBuf.V != Ea)
        free(Ea);

    return res;
}

int llistxattr(const char *path, char *namebuf, size_t size)
{
    PFILE_FULL_EA_INFORMATION Ea = 0;
    ULONG EaLength;

    EaLength = (ULONG)(FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) + 254 + 1 + 0xffff);
    Ea = malloc(EaLength); /* sets errno */
    if (0 == Ea)
        return -1;

    int res = lgetea(path, 0, 0, Ea, EaLength);
    if (0 < res)
    {
        PFILE_FULL_EA_INFORMATION EaEnd = (PVOID)((PUINT8)Ea + res);
        res = 0;
        for (PFILE_FULL_EA_INFORMATION EaPtr = Ea; EaEnd > EaPtr; EaPtr = NEXT_EA(EaPtr, EaEnd))
            res += EaPtr->EaNameLength + 1;

        if (0 == size)
            ;
        else if (res <= size)
        {
            char *p = namebuf;
            for (PFILE_FULL_EA_INFORMATION EaPtr = Ea; EaEnd > EaPtr; EaPtr = NEXT_EA(EaPtr, EaEnd))
            {
                memcpy(p, EaPtr->EaName, EaPtr->EaNameLength + 1);
                p += EaPtr->EaNameLength + 1;
            }
        }
        else
        {
            errno = ERANGE;
            res = -1;
        }
    }

    free(Ea);

    return res;
}

int lremovexattr(const char *path, const char *name)
{
    return lsetxattr(path, name, 0, 0, 0);
}

int mkdir(const char *path, fuse_mode_t mode)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    if (!CreateDirectoryW(PathBuf, 0/* default security */))
        return error();

    return 0;
}

int rmdir(const char *path)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    if (!RemoveDirectoryW(PathBuf))
        return error();

    return 0;
}

DIR *opendir(const char *path)
{
    WCHAR PathBuf[PATH_MAX];
    uncpath(path, PathBuf, PATH_MAX);
    HANDLE h = CreateFileW(PathBuf,
        FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    if (INVALID_HANDLE_VALUE == h)
        return error0();

    size_t pathlen = strlen(path);
    if (0 < pathlen && '/' == path[pathlen - 1])
        pathlen--;

    DIR *dirp = malloc(sizeof *dirp + pathlen + 3); /* sets errno */
    if (0 == dirp)
    {
        CloseHandle(h);
        return 0;
    }

    memset(dirp, 0, sizeof *dirp);
    dirp->h = h;
    dirp->fh = INVALID_HANDLE_VALUE;
    memcpy(dirp->path, path, pathlen);
    dirp->path[pathlen + 0] = '/';
    dirp->path[pathlen + 1] = '*';
    dirp->path[pathlen + 2] = '\0';

    return dirp;
}

int dirfd(DIR *dirp)
{
    return (int)(intptr_t)dirp->h;
}

void rewinddir(DIR *dirp)
{
    if (INVALID_HANDLE_VALUE != dirp->fh)
    {
        FindClose(dirp->fh);
        dirp->fh = INVALID_HANDLE_VALUE;
    }
}

struct dirent *readdir(DIR *dirp)
{
    WIN32_FIND_DATAW FindData;
    struct fuse_stat *stbuf = &dirp->de.d_stat;

    if (INVALID_HANDLE_VALUE == dirp->fh)
    {
        WCHAR PathBuf[PATH_MAX];
        uncpath(dirp->path, PathBuf, PATH_MAX);
        dirp->fh = FindFirstFileW(PathBuf, &FindData);
        if (INVALID_HANDLE_VALUE == dirp->fh)
            return error0();
    }
    else
    {
        if (!FindNextFileW(dirp->fh, &FindData))
        {
            if (ERROR_NO_MORE_FILES == GetLastError())
                return 0;
            return error0();
        }
    }

    memset(stbuf, 0, sizeof *stbuf);
    stbuf->st_mode = 0777 |
        ((FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? 0040000/* S_IFDIR */ : 0);
    stbuf->st_nlink = 1;
    stbuf->st_size = ((UINT64)FindData.nFileSizeHigh << 32) | ((UINT64)FindData.nFileSizeLow);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FindData.ftCreationTime, (void *)&stbuf->st_birthtim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FindData.ftLastAccessTime, (void *)&stbuf->st_atim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FindData.ftLastWriteTime, (void *)&stbuf->st_mtim);
    FspPosixFileTimeToUnixTime(*(PUINT64)&FindData.ftLastWriteTime, (void *)&stbuf->st_ctim);
#if defined(FSP_FUSE_USE_STAT_EX)
    stbuf->st_flags = MapFileAttributesToFlags(FindData.dwFileAttributes);
#endif

    WideCharToMultiByte(CP_UTF8, 0, FindData.cFileName, -1, dirp->de.d_name, 255 * 4, 0, 0);

    return &dirp->de;
}

int closedir(DIR *dirp)
{
    if (INVALID_HANDLE_VALUE != dirp->fh)
        FindClose(dirp->fh);

    CloseHandle(dirp->h);
    free(dirp);

    return 0;
}

static int maperror(int winerrno)
{
    switch (winerrno)
    {
    case ERROR_INVALID_FUNCTION:
        return EINVAL;
    case ERROR_FILE_NOT_FOUND:
        return ENOENT;
    case ERROR_PATH_NOT_FOUND:
        return ENOENT;
    case ERROR_TOO_MANY_OPEN_FILES:
        return EMFILE;
    case ERROR_ACCESS_DENIED:
        return EACCES;
    case ERROR_INVALID_HANDLE:
        return EBADF;
    case ERROR_ARENA_TRASHED:
        return ENOMEM;
    case ERROR_NOT_ENOUGH_MEMORY:
        return ENOMEM;
    case ERROR_INVALID_BLOCK:
        return ENOMEM;
    case ERROR_BAD_ENVIRONMENT:
        return E2BIG;
    case ERROR_BAD_FORMAT:
        return ENOEXEC;
    case ERROR_INVALID_ACCESS:
        return EINVAL;
    case ERROR_INVALID_DATA:
        return EINVAL;
    case ERROR_INVALID_DRIVE:
        return ENOENT;
    case ERROR_CURRENT_DIRECTORY:
        return EACCES;
    case ERROR_NOT_SAME_DEVICE:
        return EXDEV;
    case ERROR_NO_MORE_FILES:
        return ENOENT;
    case ERROR_LOCK_VIOLATION:
        return EACCES;
    case ERROR_BAD_NETPATH:
        return ENOENT;
    case ERROR_NETWORK_ACCESS_DENIED:
        return EACCES;
    case ERROR_BAD_NET_NAME:
        return ENOENT;
    case ERROR_FILE_EXISTS:
        return EEXIST;
    case ERROR_CANNOT_MAKE:
        return EACCES;
    case ERROR_FAIL_I24:
        return EACCES;
    case ERROR_INVALID_PARAMETER:
        return EINVAL;
    case ERROR_NO_PROC_SLOTS:
        return EAGAIN;
    case ERROR_DRIVE_LOCKED:
        return EACCES;
    case ERROR_BROKEN_PIPE:
        return EPIPE;
    case ERROR_DISK_FULL:
        return ENOSPC;
    case ERROR_INVALID_TARGET_HANDLE:
        return EBADF;
    case ERROR_WAIT_NO_CHILDREN:
        return ECHILD;
    case ERROR_CHILD_NOT_COMPLETE:
        return ECHILD;
    case ERROR_DIRECT_ACCESS_HANDLE:
        return EBADF;
    case ERROR_NEGATIVE_SEEK:
        return EINVAL;
    case ERROR_SEEK_ON_DEVICE:
        return EACCES;
    case ERROR_DIR_NOT_EMPTY:
        return ENOTEMPTY;
    case ERROR_NOT_LOCKED:
        return EACCES;
    case ERROR_BAD_PATHNAME:
        return ENOENT;
    case ERROR_MAX_THRDS_REACHED:
        return EAGAIN;
    case ERROR_LOCK_FAILED:
        return EACCES;
    case ERROR_ALREADY_EXISTS:
        return EEXIST;
    case ERROR_FILENAME_EXCED_RANGE:
        return ENOENT;
    case ERROR_NESTING_NOT_ALLOWED:
        return EAGAIN;
    case ERROR_NOT_ENOUGH_QUOTA:
        return ENOMEM;
    default:
        if (ERROR_WRITE_PROTECT <= winerrno && winerrno <= ERROR_SHARING_BUFFER_EXCEEDED)
            return EACCES;
        else if (ERROR_INVALID_STARTING_CODESEG <= winerrno && winerrno <= ERROR_INFLOOP_IN_RELOC_CHAIN)
            return ENOEXEC;
        else
            return EINVAL;
    }
}

long WinFspLoad(void)
{
    return FspLoad(0);
}
