/**
 * @file CustomActions.cpp
 *
 * @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.
 */

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <msiquery.h>
#include <wcautil.h>
#include <strutil.h>

static HINSTANCE DllInstance;

UINT __stdcall InstanceID(MSIHANDLE MsiHandle)
{
#if 0
    WCHAR MessageBuf[64];
    wsprintfW(MessageBuf, L"PID=%ld", GetCurrentProcessId());
    MessageBoxW(0, MessageBuf, L"" __FUNCTION__ " Break", MB_OK);
#endif

    HRESULT hr = S_OK;
    UINT err = ERROR_SUCCESS;
    SYSTEMTIME SystemTime;
    WCHAR Result[32+1];

    hr = WcaInitialize(MsiHandle, __FUNCTION__);
    ExitOnFailure(hr, "Failed to initialize");

    WcaLog(LOGMSG_STANDARD, "Initialized");

    GetSystemTime(&SystemTime);
    wsprintfW(Result, L"%04u%02u%02uT%02u%02u%02uZ",
        SystemTime.wYear,
        SystemTime.wMonth,
        SystemTime.wDay,
        SystemTime.wHour,
        SystemTime.wMinute,
        SystemTime.wSecond);

    /*
     * Sleep 1 second to ensure timestamp uniqueness.
     *
     * Note that this assumes that time is monotonic and users do not change time.
     * Disable for now as it is assumed that the installation takes more than 1 second to complete.
     */
    //Sleep(1000);

    WcaSetProperty(L"" __FUNCTION__, Result);

LExit:
    err = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return WcaFinalize(err);
}

UINT __stdcall ServiceRunning(MSIHANDLE MsiHandle)
{
#if 0
    WCHAR MessageBuf[64];
    wsprintfW(MessageBuf, L"PID=%ld", GetCurrentProcessId());
    MessageBoxW(0, MessageBuf, L"" __FUNCTION__ " Break", MB_OK);
#endif

    HRESULT hr = S_OK;
    UINT err = ERROR_SUCCESS;
    PWSTR ServiceName = 0;
    SC_HANDLE ScmHandle = 0;
    SC_HANDLE SvcHandle = 0;
    SERVICE_STATUS ServiceStatus;
    int Result = 0;

    hr = WcaInitialize(MsiHandle, __FUNCTION__);
    ExitOnFailure(hr, "Failed to initialize");

    hr = WcaGetProperty(L"" __FUNCTION__, &ServiceName);
    ExitOnFailure(hr, "Failed to get ServiceName");

    WcaLog(LOGMSG_STANDARD, "Initialized: \"%S\"", ServiceName);

    ScmHandle = OpenSCManagerW(0, 0, 0);
    ExitOnNullWithLastError(ScmHandle, hr, "Failed to open SCM");

    SvcHandle = OpenServiceW(ScmHandle, ServiceName, SERVICE_QUERY_STATUS);
    if (0 != SvcHandle && QueryServiceStatus(SvcHandle, &ServiceStatus))
        Result = SERVICE_STOPPED != ServiceStatus.dwCurrentState;

    WcaSetIntProperty(L"" __FUNCTION__, Result);

LExit:
    if (0 != SvcHandle)
        CloseServiceHandle(SvcHandle);
    if (0 != ScmHandle)
        CloseServiceHandle(ScmHandle);

    ReleaseStr(ServiceName);

    err = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return WcaFinalize(err);
}

UINT __stdcall DeferredAction(MSIHANDLE MsiHandle)
{
#if 0
    WCHAR MessageBuf[64];
    wsprintfW(MessageBuf, L"PID=%ld", GetCurrentProcessId());
    MessageBoxW(0, MessageBuf, L"" __FUNCTION__ " Break", MB_OK);
#endif

    HRESULT hr = S_OK;
    UINT err = ERROR_SUCCESS;
    PWSTR CommandLine = 0;
    PWSTR *Argv;
    int Argc;
    CHAR ProcName[64];
    FARPROC Proc;

    hr = WcaInitialize(MsiHandle, __FUNCTION__);
    ExitOnFailure(hr, "Failed to initialize");

    hr = WcaGetProperty(L"CustomActionData", &CommandLine);
    ExitOnFailure(hr, "Failed to get CommandLine");

    WcaLog(LOGMSG_STANDARD, "Initialized: \"%S\"", CommandLine);

    Argv = CommandLineToArgvW(CommandLine, &Argc);
    ExitOnNullWithLastError(Argv, hr, "Failed to CommandLineToArgvW");

    if (0 < Argc)
    {
        if (0 == WideCharToMultiByte(CP_UTF8, 0, Argv[0], -1, ProcName, sizeof ProcName, 0, 0))
            ExitWithLastError(hr, "Failed to WideCharToMultiByte");

        Proc = GetProcAddress(DllInstance, ProcName);
        ExitOnNullWithLastError(Proc, hr, "Failed to GetProcAddress");

        err = ((HRESULT (*)(int, PWSTR *))Proc)(Argc, Argv);
        ExitOnWin32Error(err, hr, "Failed to %s", ProcName);
    }
    else
    {
        hr = E_INVALIDARG;
        ExitOnFailure(hr, "Failed to get arguments");
    }

LExit:
    LocalFree(Argv);
    ReleaseStr(CommandLine);

    err = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return WcaFinalize(err);
}

static DWORD MakeSymlink(PWSTR Symlink, PWSTR Target);
static DWORD MakeJunction(PWSTR Junction, PWSTR Target);
static DWORD CreateJunction(PWSTR Junction, PWSTR Target);
static DWORD RemoveFile(PWSTR FileName);

DWORD InstallSymlinks(int Argc, PWSTR *Argv)
{
    /* usage: InstallSymlinks/InstallJunctions SourceDir TargetDir Name... */

    DWORD Result;
    BOOL Junctions;
    PWSTR SourceDir, TargetDir;
    WCHAR SourcePath[MAX_PATH], TargetPath[MAX_PATH];
    int SourceDirLen, TargetDirLen, Len;

    if (4 > Argc)
    {
        Result = ERROR_INVALID_PARAMETER;
        goto exit;
    }

    Junctions = 0 == lstrcmpW(L"InstallJunctions", Argv[0]);
    SourceDir = Argv[1];
    TargetDir = Argv[2];
    SourceDirLen = lstrlenW(SourceDir);
    TargetDirLen = lstrlenW(TargetDir);

    for (int Argi = 3; Argc > Argi; Argi++)
    {
        Len = lstrlenW(Argv[Argi]);
        if (MAX_PATH < SourceDirLen + Len + 1 || MAX_PATH < TargetDirLen + Len + 1)
        {
            Result = ERROR_FILENAME_EXCED_RANGE;
            goto exit;
        }

        memcpy(SourcePath, SourceDir, SourceDirLen * sizeof(WCHAR));
        memcpy(SourcePath + SourceDirLen, Argv[Argi], Len * sizeof(WCHAR));
        SourcePath[SourceDirLen + Len] = L'\0';

        memcpy(TargetPath, TargetDir, TargetDirLen * sizeof(WCHAR));
        memcpy(TargetPath + TargetDirLen, Argv[Argi], Len * sizeof(WCHAR));
        TargetPath[TargetDirLen + Len] = L'\0';

        if (!Junctions)
            Result = MakeSymlink(SourcePath, TargetPath);
        else
            Result = MakeJunction(SourcePath, TargetPath);
#if 0
    WCHAR MessageBuf[1024];
    wsprintfW(MessageBuf, L"MakeSymlink(\"%s\", \"%s\") = %lu", SourcePath, TargetPath, Result);
    MessageBoxW(0, MessageBuf, L"TRACE", MB_OK);
#endif
        if (ERROR_SUCCESS != Result)
            goto exit;
    }

    Result = ERROR_SUCCESS;

exit:
    return Result;
}

DWORD InstallJunctions(int Argc, PWSTR *Argv)
{
    return InstallSymlinks(Argc, Argv);
}

DWORD RemoveFiles(int Argc, PWSTR *Argv)
{
    /* usage: RemoveFiles Dir Name... */

    DWORD Result;
    PWSTR Dir;
    WCHAR Path[MAX_PATH];
    int DirLen, Len;

    if (3 > Argc)
    {
        Result = ERROR_INVALID_PARAMETER;
        goto exit;
    }

    Dir = Argv[1];
    DirLen = lstrlenW(Dir);

    for (int Argi = 2; Argc > Argi; Argi++)
    {
        Len = lstrlenW(Argv[Argi]);
        if (MAX_PATH < DirLen + Len + 1)
        {
            Result = ERROR_FILENAME_EXCED_RANGE;
            goto exit;
        }

        memcpy(Path, Dir, DirLen * sizeof(WCHAR));
        memcpy(Path + DirLen, Argv[Argi], Len * sizeof(WCHAR));
        Path[DirLen + Len] = L'\0';

        Result = RemoveFile(Path);
#if 0
    WCHAR MessageBuf[1024];
    wsprintfW(MessageBuf, L"RemoveFile(\"%s\") = %lu", Path, Result);
    MessageBoxW(0, MessageBuf, L"TRACE", MB_OK);
#endif
        if (ERROR_SUCCESS != Result)
            goto exit;
    }

    Result = ERROR_SUCCESS;

exit:
    return Result;
}

static DWORD MakeSymlink(PWSTR Symlink, PWSTR Target)
{
    DWORD Result;
    DWORD FileAttributes, Flags;

    RemoveFile(Symlink);

    FileAttributes = GetFileAttributesW(Target);
    if (INVALID_FILE_ATTRIBUTES == FileAttributes)
    {
        Result = GetLastError();
        goto exit;
    }
    Flags = 0 != (FileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0;

    if (!CreateSymbolicLinkW(Symlink, Target, Flags))
    {
        Result = GetLastError();
        RemoveFile(Symlink);
        goto exit;
    }

    Result = ERROR_SUCCESS;

exit:
    return Result;
}

static DWORD MakeJunction(PWSTR Junction, PWSTR Target)
{
    DWORD Result;
    DWORD FileAttributes;

    RemoveFile(Junction);

    FileAttributes = GetFileAttributesW(Target);
    if (INVALID_FILE_ATTRIBUTES == FileAttributes)
    {
        Result = GetLastError();
        goto exit;
    }
    if (0 == (FileAttributes & FILE_ATTRIBUTE_DIRECTORY))
    {
        Result = ERROR_DIRECTORY;
        goto exit;
    }

    Result = CreateJunction(Junction, Target);
    if (ERROR_SUCCESS != Result)
    {
        RemoveFile(Junction);
        goto exit;
    }

    Result = ERROR_SUCCESS;

exit:
    return Result;
}

static DWORD CreateJunction(PWSTR Junction, PWSTR Target)
{
    /*
     * The REPARSE_DATA_BUFFER definitions appear to be missing from the user mode headers.
     */
    typedef struct _REPARSE_DATA_BUFFER
    {
        ULONG ReparseTag;
        USHORT ReparseDataLength;
        USHORT Reserved;
        union
        {
            struct
            {
                USHORT SubstituteNameOffset;
                USHORT SubstituteNameLength;
                USHORT PrintNameOffset;
                USHORT PrintNameLength;
                ULONG Flags;
                WCHAR PathBuffer[1];
            } SymbolicLinkReparseBuffer;
            struct
            {
                USHORT SubstituteNameOffset;
                USHORT SubstituteNameLength;
                USHORT PrintNameOffset;
                USHORT PrintNameLength;
                WCHAR PathBuffer[1];
            } MountPointReparseBuffer;
            struct
            {
                UCHAR DataBuffer[1];
            } GenericReparseBuffer;
        } DUMMYUNIONNAME;
    } REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
    const LONG REPARSE_DATA_BUFFER_HEADER_SIZE =
        FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer);
    const DWORD FSCTL_SET_REPARSE_POINT = 0x000900a4;

    DWORD Result;
    HANDLE Handle = INVALID_HANDLE_VALUE;
    USHORT TargetLength, ReparseDataLength;
    PREPARSE_DATA_BUFFER ReparseData = 0;
    PWSTR PathBuffer;
    DWORD Bytes;

    if (!(
        ((L'A' <= Target[0] && Target[0] <= L'Z') || (L'a' <= Target[0] && Target[0] <= L'z')) &&
        L':' == Target[1]
        ))
    {
        Result = ERROR_INVALID_NAME;
        goto exit;
    }

    Handle = CreateFileW(Junction,
        FILE_WRITE_ATTRIBUTES,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        0,
        CREATE_NEW,
        FILE_ATTRIBUTE_DIRECTORY |
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_POSIX_SEMANTICS,
        0);
    if (INVALID_HANDLE_VALUE == Handle)
    {
        Result = GetLastError();
        goto exit;
    }

    TargetLength = (USHORT)lstrlenW(Target) * sizeof(WCHAR);
    ReparseDataLength = (USHORT)(
        FIELD_OFFSET(REPARSE_DATA_BUFFER, MountPointReparseBuffer.PathBuffer) -
        FIELD_OFFSET(REPARSE_DATA_BUFFER, MountPointReparseBuffer)) +
        4 * sizeof(WCHAR) + 2 * (TargetLength + sizeof(WCHAR));
    ReparseData = (PREPARSE_DATA_BUFFER)
        HeapAlloc(GetProcessHeap(), 0, REPARSE_DATA_BUFFER_HEADER_SIZE + ReparseDataLength);
    if (0 == ReparseData)
    {
        Result = ERROR_NO_SYSTEM_RESOURCES;
        goto exit;
    }

    ReparseData->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
    ReparseData->ReparseDataLength = ReparseDataLength;
    ReparseData->Reserved = 0;
    ReparseData->MountPointReparseBuffer.SubstituteNameOffset = 0;
    ReparseData->MountPointReparseBuffer.SubstituteNameLength =
        4 * sizeof(WCHAR) + TargetLength;
    ReparseData->MountPointReparseBuffer.PrintNameOffset =
        ReparseData->MountPointReparseBuffer.SubstituteNameLength + sizeof(WCHAR);
    ReparseData->MountPointReparseBuffer.PrintNameLength =
        TargetLength;

    PathBuffer = ReparseData->MountPointReparseBuffer.PathBuffer;
    PathBuffer[0] = L'\\';
    PathBuffer[1] = L'?';
    PathBuffer[2] = L'?';
    PathBuffer[3] = L'\\';
    memcpy(PathBuffer + 4, Target, TargetLength);
    PathBuffer[4 + TargetLength / sizeof(WCHAR)] = L'\0';

    PathBuffer = ReparseData->MountPointReparseBuffer.PathBuffer +
        (ReparseData->MountPointReparseBuffer.PrintNameOffset) / sizeof(WCHAR);
    memcpy(PathBuffer, Target, TargetLength);
    PathBuffer[TargetLength / sizeof(WCHAR)] = L'\0';

    if (!DeviceIoControl(Handle, FSCTL_SET_REPARSE_POINT,
        ReparseData, REPARSE_DATA_BUFFER_HEADER_SIZE + ReparseData->ReparseDataLength,
        0, 0,
        &Bytes, 0))
    {
        Result = GetLastError();
        goto exit;
    }

    Result = ERROR_SUCCESS;

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

    if (0 != ReparseData)
        HeapFree(GetProcessHeap(), 0, ReparseData);

    return Result;
}

static DWORD RemoveFile(PWSTR FileName)
{
    DWORD Result;

    if (!RemoveDirectoryW(FileName))
    {
        Result = GetLastError();
        if (ERROR_DIRECTORY != Result)
            goto exit;

        if (!DeleteFileW(FileName))
        {
            Result = GetLastError();
            goto exit;
        }
    }

    Result = ERROR_SUCCESS;

exit:
    return Result;
}

extern "C"
BOOL WINAPI DllMain(HINSTANCE Instance, DWORD Reason, PVOID Reserved)
{
    switch(Reason)
    {
    case DLL_PROCESS_ATTACH:
        DllInstance = Instance;
        WcaGlobalInitialize(Instance);
        break;
    case DLL_PROCESS_DETACH:
        WcaGlobalFinalize();
        break;
    }

    return TRUE;
}
