/**
 * @file shared/ku/mountmgr.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.
 */

#include <shared/ku/library.h>

#pragma warning(push)
#pragma warning(disable:4459)           /* declaration of 'identifier' hides global declaration */

static NTSTATUS FspMountmgrControl(ULONG IoControlCode,
    PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, PULONG POutputBufferLength);
static NTSTATUS FspMountmgrNotifyVolumeArrival(
    PUNICODE_STRING VolumeName, GUID *UniqueId);
static NTSTATUS FspMountmgrNotifyMountPoint(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint, BOOLEAN Created);
static NTSTATUS FspMountmgrCreateMountPoint(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint);
static NTSTATUS FspMountmgrDeleteMountPoint(
    PUNICODE_STRING MountPoint);
static VOID FspMountmgrDeleteRegistry(
    GUID *UniqueId);
NTSTATUS FspMountmgrCreateDrive(
    PUNICODE_STRING VolumeName, GUID *UniqueId, PUNICODE_STRING MountPoint);
NTSTATUS FspMountmgrDeleteDrive(
    PUNICODE_STRING MountPoint);
NTSTATUS FspMountmgrNotifyCreateDirectory(
    PUNICODE_STRING VolumeName, GUID *UniqueId, PUNICODE_STRING MountPoint);
NTSTATUS FspMountmgrNotifyDeleteDirectory(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint);

#if defined(_KERNEL_MODE)
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, FspMountmgrControl)
#pragma alloc_text(PAGE, FspMountmgrNotifyVolumeArrival)
#pragma alloc_text(PAGE, FspMountmgrNotifyMountPoint)
#pragma alloc_text(PAGE, FspMountmgrCreateMountPoint)
#pragma alloc_text(PAGE, FspMountmgrDeleteMountPoint)
#pragma alloc_text(PAGE, FspMountmgrCreateDrive)
#pragma alloc_text(PAGE, FspMountmgrDeleteDrive)
#pragma alloc_text(PAGE, FspMountmgrNotifyCreateDirectory)
#pragma alloc_text(PAGE, FspMountmgrNotifyDeleteDirectory)
#endif
#endif

static NTSTATUS FspMountmgrControl(ULONG IoControlCode,
    PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, PULONG POutputBufferLength)
{
#if defined(_KERNEL_MODE)
    FSP_KU_CODE;

    UNICODE_STRING DeviceName;
    OBJECT_ATTRIBUTES ObjectAttributes;
    IO_STATUS_BLOCK IoStatus;
    HANDLE MgrHandle = 0;
    ULONG Bytes = 0;

    if (0 == POutputBufferLength)
        POutputBufferLength = &Bytes;

    RtlInitUnicodeString(&DeviceName, L"\\Device\\MountPointManager");
    InitializeObjectAttributes(
        &ObjectAttributes,
        &DeviceName,
        OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
        0/*RootDirectory*/,
        0);

    IoStatus.Status = ZwOpenFile(
        &MgrHandle,
        GENERIC_READ | GENERIC_WRITE,
        &ObjectAttributes,
        &IoStatus,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        FILE_SYNCHRONOUS_IO_ALERT);
    if (!NT_SUCCESS(IoStatus.Status))
        goto exit;

    IoStatus.Status = ZwDeviceIoControlFile(
        MgrHandle,
        0, 0, 0,
        &IoStatus,
        IoControlCode,
        InputBuffer, InputBufferLength, OutputBuffer, *POutputBufferLength);
    if (!NT_SUCCESS(IoStatus.Status))
        goto exit;

    *POutputBufferLength = (ULONG)IoStatus.Information;
    IoStatus.Status = STATUS_SUCCESS;

exit:
    if (0 != MgrHandle)
        ZwClose(MgrHandle);

    return IoStatus.Status;
#else
    HANDLE MgrHandle = INVALID_HANDLE_VALUE;
    DWORD Bytes = 0;
    NTSTATUS Result;

    if (0 == POutputBufferLength)
        POutputBufferLength = &Bytes;

    MgrHandle = CreateFileW(L"\\\\.\\MountPointManager",
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        0,
        OPEN_EXISTING,
        0,
        0);
    if (INVALID_HANDLE_VALUE == MgrHandle)
    {
        Result = FspNtStatusFromWin32(GetLastError());
        goto exit;
    }

    if (!DeviceIoControl(MgrHandle,
        IoControlCode,
        InputBuffer, InputBufferLength, OutputBuffer, *POutputBufferLength,
        &Bytes, 0))
    {
        Result = FspNtStatusFromWin32(GetLastError());
        goto exit;
    }

    *POutputBufferLength = Bytes;
    Result = STATUS_SUCCESS;

exit:
    if (INVALID_HANDLE_VALUE != MgrHandle)
        CloseHandle(MgrHandle);

    return Result;
#endif
}

static NTSTATUS FspMountmgrNotifyVolumeArrival(
    PUNICODE_STRING VolumeName, GUID *UniqueId)
{
    FSP_KU_CODE;

    /* mountmgr.h */
    typedef enum
    {
        Disabled = 0,
        Enabled,
    } MOUNTMGR_AUTO_MOUNT_STATE;
    typedef struct
    {
        MOUNTMGR_AUTO_MOUNT_STATE CurrentState;
    } MOUNTMGR_QUERY_AUTO_MOUNT;
    typedef struct
    {
        MOUNTMGR_AUTO_MOUNT_STATE NewState;
    } MOUNTMGR_SET_AUTO_MOUNT;
    typedef struct
    {
        USHORT DeviceNameLength;
        WCHAR DeviceName[1];
    } MOUNTMGR_TARGET_NAME;

    MOUNTMGR_QUERY_AUTO_MOUNT QueryAutoMount;
    MOUNTMGR_SET_AUTO_MOUNT SetAutoMount;
    MOUNTMGR_TARGET_NAME *TargetName = 0;
    ULONG QueryAutoMountSize, TargetNameSize;
    NTSTATUS Result;

    QueryAutoMountSize = sizeof QueryAutoMount;
    TargetNameSize = FIELD_OFFSET(MOUNTMGR_TARGET_NAME, DeviceName) + VolumeName->Length;

    TargetName = MemAlloc(TargetNameSize);
    if (0 == TargetName)
    {
        Result = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    /* query the current AutoMount value and save it */
    Result = FspMountmgrControl(
        CTL_CODE('m', 15, METHOD_BUFFERED, FILE_ANY_ACCESS),
            /* IOCTL_MOUNTMGR_QUERY_AUTO_MOUNT */
        0, 0, &QueryAutoMount, &QueryAutoMountSize);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* disable AutoMount */
    SetAutoMount.NewState = 0;
    Result = FspMountmgrControl(
        CTL_CODE('m', 16, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS),
            /* IOCTL_MOUNTMGR_SET_AUTO_MOUNT */
        &SetAutoMount, sizeof SetAutoMount, 0, 0);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* announce volume arrival */
    memset(TargetName, 0, sizeof *TargetName);
    TargetName->DeviceNameLength = VolumeName->Length;
    memcpy(TargetName->DeviceName, VolumeName->Buffer, VolumeName->Length);
    Result = FspMountmgrControl(
        CTL_CODE('m', 11, METHOD_BUFFERED, FILE_READ_ACCESS),
            /* IOCTL_MOUNTMGR_VOLUME_ARRIVAL_NOTIFICATION */
        TargetName, TargetNameSize, 0, 0);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* reset the AutoMount value to the saved one */
    SetAutoMount.NewState = QueryAutoMount.CurrentState;
    FspMountmgrControl(
        CTL_CODE('m', 16, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS),
            /* IOCTL_MOUNTMGR_SET_AUTO_MOUNT */
        &SetAutoMount, sizeof SetAutoMount, 0, 0);

    Result = STATUS_SUCCESS;

exit:
    MemFree(TargetName);

    return Result;
}

static NTSTATUS FspMountmgrNotifyMountPoint(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint, BOOLEAN Created)
{
    FSP_KU_CODE;

    /* mountmgr.h */
    typedef struct
    {
        USHORT SourceVolumeNameOffset;
        USHORT SourceVolumeNameLength;
        USHORT TargetVolumeNameOffset;
        USHORT TargetVolumeNameLength;
    } MOUNTMGR_VOLUME_MOUNT_POINT;

    MOUNTMGR_VOLUME_MOUNT_POINT *VolumeMountPoint = 0;
    ULONG VolumeMountPointSize;
    NTSTATUS Result;

    VolumeMountPointSize = sizeof *VolumeMountPoint +
        sizeof L"\\DosDevices\\" - sizeof(WCHAR) + MountPoint->Length + VolumeName->Length;

    VolumeMountPoint = MemAlloc(VolumeMountPointSize);
    if (0 == VolumeMountPoint)
    {
        Result = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    /* notify volume mount point created/deleted */
    memset(VolumeMountPoint, 0, sizeof *VolumeMountPoint);
    VolumeMountPoint->SourceVolumeNameOffset = sizeof *VolumeMountPoint;
    VolumeMountPoint->SourceVolumeNameLength = (USHORT)(
        sizeof L"\\DosDevices\\" - sizeof(WCHAR) + MountPoint->Length);
    VolumeMountPoint->TargetVolumeNameOffset =
        VolumeMountPoint->SourceVolumeNameOffset + VolumeMountPoint->SourceVolumeNameLength;
    VolumeMountPoint->TargetVolumeNameLength = VolumeName->Length;
    memcpy((PUINT8)VolumeMountPoint + VolumeMountPoint->SourceVolumeNameOffset,
        L"\\DosDevices\\", sizeof L"\\DosDevices\\" - sizeof(WCHAR));
    memcpy((PUINT8)VolumeMountPoint +
        VolumeMountPoint->SourceVolumeNameOffset + (sizeof L"\\DosDevices\\" - sizeof(WCHAR)),
        MountPoint->Buffer, MountPoint->Length);
    memcpy((PUINT8)VolumeMountPoint + VolumeMountPoint->TargetVolumeNameOffset,
        VolumeName->Buffer, VolumeName->Length);
    Result = FspMountmgrControl(
        Created ?
            CTL_CODE('m', 6, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) :
                /* IOCTL_MOUNTMGR_VOLUME_MOUNT_POINT_CREATED */
            CTL_CODE('m', 7, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS),
                /* IOCTL_MOUNTMGR_VOLUME_MOUNT_POINT_DELETED */
        VolumeMountPoint, VolumeMountPointSize, 0, 0);
    if (!NT_SUCCESS(Result))
        goto exit;

    Result = STATUS_SUCCESS;

exit:
    MemFree(VolumeMountPoint);

    return Result;
}

static NTSTATUS FspMountmgrCreateMountPoint(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    /* mountmgr.h */
    typedef struct
    {
        USHORT SymbolicLinkNameOffset;
        USHORT SymbolicLinkNameLength;
        USHORT DeviceNameOffset;
        USHORT DeviceNameLength;
    } MOUNTMGR_CREATE_POINT_INPUT;

    MOUNTMGR_CREATE_POINT_INPUT *CreatePointInput = 0;
    ULONG CreatePointInputSize;
    NTSTATUS Result;

    CreatePointInputSize = sizeof *CreatePointInput +
        sizeof L"\\DosDevices\\X:" - sizeof(WCHAR) + VolumeName->Length;

    CreatePointInput = MemAlloc(CreatePointInputSize);
    if (0 == CreatePointInput)
    {
        Result = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    /* create mount point */
    memset(CreatePointInput, 0, sizeof *CreatePointInput);
    CreatePointInput->SymbolicLinkNameOffset = sizeof *CreatePointInput;
    CreatePointInput->SymbolicLinkNameLength = sizeof L"\\DosDevices\\X:" - sizeof(WCHAR);
    CreatePointInput->DeviceNameOffset =
        CreatePointInput->SymbolicLinkNameOffset + CreatePointInput->SymbolicLinkNameLength;
    CreatePointInput->DeviceNameLength = VolumeName->Length;
    memcpy((PUINT8)CreatePointInput + CreatePointInput->SymbolicLinkNameOffset,
        L"\\DosDevices\\X:", CreatePointInput->SymbolicLinkNameLength);
    ((PWCHAR)((PUINT8)CreatePointInput + CreatePointInput->SymbolicLinkNameOffset))[12] =
        MountPoint->Buffer[0] & ~0x20;
        /* convert to uppercase */
    memcpy((PUINT8)CreatePointInput + CreatePointInput->DeviceNameOffset,
        VolumeName->Buffer, VolumeName->Length);
    Result = FspMountmgrControl(
        CTL_CODE('m', 0, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS),
            /* IOCTL_MOUNTMGR_CREATE_POINT */
        CreatePointInput, CreatePointInputSize, 0, 0);
    if (!NT_SUCCESS(Result))
        goto exit;

    Result = STATUS_SUCCESS;

exit:
    MemFree(CreatePointInput);

    return Result;
}

static NTSTATUS FspMountmgrDeleteMountPoint(
    PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    /* mountmgr.h */
    typedef struct
    {
        ULONG SymbolicLinkNameOffset;
        USHORT SymbolicLinkNameLength;
        USHORT Reserved1;
        ULONG UniqueIdOffset;
        USHORT UniqueIdLength;
        USHORT Reserved2;
        ULONG DeviceNameOffset;
        USHORT DeviceNameLength;
        USHORT Reserved3;
    } MOUNTMGR_MOUNT_POINT;
    typedef struct
    {
        ULONG Size;
        ULONG NumberOfMountPoints;
        MOUNTMGR_MOUNT_POINT MountPoints[1];
    } MOUNTMGR_MOUNT_POINTS;

    MOUNTMGR_MOUNT_POINT *Input = 0;
    MOUNTMGR_MOUNT_POINTS *Output = 0;
    ULONG InputSize, OutputSize;
    NTSTATUS Result;

    InputSize = sizeof *Input + sizeof L"\\DosDevices\\X:" - sizeof(WCHAR);
    OutputSize = 4096;

    Input = MemAlloc(InputSize);
    if (0 == Input)
    {
        Result = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    Output = MemAlloc(OutputSize);
    if (0 == Output)
    {
        Result = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    /* delete mount point */
    memset(Input, 0, sizeof *Input);
    Input->SymbolicLinkNameOffset = sizeof *Input;
    Input->SymbolicLinkNameLength = sizeof L"\\DosDevices\\X:" - sizeof(WCHAR);
    memcpy((PUINT8)Input + Input->SymbolicLinkNameOffset,
        L"\\DosDevices\\X:", Input->SymbolicLinkNameLength);
    ((PWCHAR)((PUINT8)Input + Input->SymbolicLinkNameOffset))[12] =
        MountPoint->Buffer[0] & ~0x20;
        /* convert to uppercase */
    Result = FspMountmgrControl(
        CTL_CODE('m', 1, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS),
            /* IOCTL_MOUNTMGR_DELETE_POINTS */
        Input, InputSize, Output, &OutputSize);
    if (!NT_SUCCESS(Result))
        goto exit;

    Result = STATUS_SUCCESS;

exit:
    MemFree(Output);
    MemFree(Input);

    return Result;
}

static VOID FspMountmgrDeleteRegistry(GUID *UniqueId)
{
#if defined(_KERNEL_MODE)
    FSP_KU_CODE;

    UNICODE_STRING Path;
    OBJECT_ATTRIBUTES ObjectAttributes;
    HANDLE Handle = 0;
    union
    {
        KEY_VALUE_FULL_INFORMATION V;
        UINT8 B[FIELD_OFFSET(KEY_VALUE_FULL_INFORMATION, Name) + 255 + sizeof *UniqueId];
    } FullInformation;
    ULONG FullInformationLength;
    UNICODE_STRING ValueName;
    NTSTATUS Result;

    RtlInitUnicodeString(&Path, L"\\Registry\\Machine\\System\\MountedDevices");
    InitializeObjectAttributes(
        &ObjectAttributes,
        &Path,
        OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
        0/*RootDirectory*/,
        0);

    Result = ZwOpenKey(&Handle, KEY_QUERY_VALUE, &ObjectAttributes);
    if (NT_SUCCESS(Result))
    {
        for (ULONG I = 0;; I++)
        {
            Result = ZwEnumerateValueKey(Handle,
                I, KeyValueFullInformation, &FullInformation,
                sizeof FullInformation, &FullInformationLength);
            if (STATUS_NO_MORE_ENTRIES == Result)
                break;
            else if (!NT_SUCCESS(Result))
                continue;

            if (REG_BINARY == FullInformation.V.Type &&
                sizeof *UniqueId == FullInformation.V.DataLength &&
                InlineIsEqualGUID((GUID *)((PUINT8)&FullInformation.V + FullInformation.V.DataOffset),
                    UniqueId))
            {
                ValueName.Length = ValueName.MaximumLength = (USHORT)FullInformation.V.NameLength;
                ValueName.Buffer = FullInformation.V.Name;
                Result = ZwDeleteValueKey(Handle, &ValueName);
                if (NT_SUCCESS(Result))
                    /* reset index after modifying key; only safe way to use RegEnumValueW with modifications */
                    I = (ULONG)-1;
            }
        }

        ZwClose(Handle);
    }
#else
    HKEY RegKey;
    LONG RegResult;
    WCHAR RegValueName[MAX_PATH];
    UINT8 RegValueData[sizeof *UniqueId];
    DWORD RegValueNameSize, RegValueDataSize;
    DWORD RegType;

    RegResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"System\\MountedDevices",
        0, KEY_READ | KEY_WRITE, &RegKey);
    if (ERROR_SUCCESS == RegResult)
    {
        for (DWORD I = 0;; I++)
        {
            RegValueNameSize = MAX_PATH;
            RegValueDataSize = sizeof RegValueData;
            RegResult = RegEnumValueW(RegKey,
                I, RegValueName, &RegValueNameSize, 0, &RegType, RegValueData, &RegValueDataSize);
            if (ERROR_NO_MORE_ITEMS == RegResult)
                break;
            else if (ERROR_SUCCESS != RegResult)
                continue;

            if (REG_BINARY == RegType &&
                sizeof RegValueData == RegValueDataSize &&
                InlineIsEqualGUID((GUID *)&RegValueData, UniqueId))
            {
                RegResult = RegDeleteValueW(RegKey, RegValueName);
                if (ERROR_SUCCESS == RegResult)
                    /* reset index after modifying key; only safe way to use RegEnumValueW with modifications */
                    I = -1;
            }
        }

        RegCloseKey(RegKey);
    }
#endif
}

NTSTATUS FspMountmgrCreateDrive(
    PUNICODE_STRING VolumeName, GUID *UniqueId, PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    NTSTATUS Result;

    /* notify the MountManager about the new volume */
    Result = FspMountmgrNotifyVolumeArrival(VolumeName, UniqueId);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* use the MountManager to create the drive */
    Result = FspMountmgrCreateMountPoint(VolumeName, MountPoint);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* HACK: delete the MountManager registry entries */
    FspMountmgrDeleteRegistry(UniqueId);

    Result = STATUS_SUCCESS;

exit:
    return Result;
}

NTSTATUS FspMountmgrDeleteDrive(
    PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    NTSTATUS Result;

    /* use the MountManager to delete the drive */
    Result = FspMountmgrDeleteMountPoint(MountPoint);
    if (!NT_SUCCESS(Result))
        goto exit;

    Result = STATUS_SUCCESS;

exit:
    return Result;
}

NTSTATUS FspMountmgrNotifyCreateDirectory(
    PUNICODE_STRING VolumeName, GUID *UniqueId, PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    NTSTATUS Result;

    /* notify the MountManager about the new volume */
    Result = FspMountmgrNotifyVolumeArrival(VolumeName, UniqueId);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* notify the MountManager about the created directory mount point */
    Result = FspMountmgrNotifyMountPoint(VolumeName, MountPoint, TRUE);
    if (!NT_SUCCESS(Result))
        goto exit;

    /* HACK: delete the MountManager registry entries */
    FspMountmgrDeleteRegistry(UniqueId);

    Result = STATUS_SUCCESS;

exit:
    return Result;
}

NTSTATUS FspMountmgrNotifyDeleteDirectory(
    PUNICODE_STRING VolumeName, PUNICODE_STRING MountPoint)
{
    FSP_KU_CODE;

    /* notify the MountManager about the deleted directory mount point */
    return FspMountmgrNotifyMountPoint(VolumeName, MountPoint, FALSE);
}

#pragma warning(pop)
