// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "savefile.h"

#include "filepath.h"
#include "hostosinfo.h"
#include "qtcassert.h"

#ifdef Q_OS_WIN
#  include <windows.h>
#  include <io.h>
#else
#  include <unistd.h>
#  include <sys/stat.h>
#endif

#ifdef Q_OS_LINUX
#  include <sys/xattr.h>
#endif

namespace Utils {

static QFile::Permissions m_umask;

#ifdef Q_OS_LINUX
// setPermissions() restores only the classic rwx mode bits, not the POSIX ACL.
// On file systems that carry permissions in an ACL (notably Samba/CIFS mounts,
// where the executable bit lives in the ACL) a plain mode-bit copy silently
// drops it, e.g. clearing the execute permission on every save. Copy the access
// ACL verbatim through its extended attribute. Best-effort; ignore errors.
static void copyPosixAccessAcl(const QString &from, const QString &to)
{
    static const char aclAttr[] = "system.posix_acl_access";
    const QByteArray fromPath = QFile::encodeName(from);
    ssize_t size = getxattr(fromPath.constData(), aclAttr, nullptr, 0);
    if (size <= 0)
        return; // No ACL, or not supported by the file system.
    QByteArray blob(size, '\0');
    size = getxattr(fromPath.constData(), aclAttr, blob.data(), blob.size());
    if (size <= 0)
        return;
    setxattr(QFile::encodeName(to).constData(), aclAttr, blob.constData(), size, 0);
}
#endif

SaveFile::SaveFile(const FilePath &filePath) :
    m_finalFilePath(filePath)
{
}

SaveFile::~SaveFile()
{
    QTC_ASSERT(m_finalized, rollback());
}

bool SaveFile::open(OpenMode flags)
{
    QTC_ASSERT(!m_finalFilePath.isEmpty(), return false);
    QTC_ASSERT(!m_tempFile, return false);

    QFile ofi(m_finalFilePath.toFSPathString());
    // Check whether the existing file is writable
    if (ofi.exists() && !ofi.open(QIODevice::ReadWrite)) {
        setErrorString(ofi.errorString());
        return false;
    }

    auto tmpPath = TemporaryFilePath::create(m_finalFilePath);
    if (!tmpPath) {
        setErrorString(tmpPath.error());
        return false;
    }
    m_tempFile = std::move(*tmpPath);
    m_tempFile->setAutoRemove(false);

    setFileName(m_tempFile->filePath().toFSPathString());

    if (!QFile::open(flags))
        return false;

    m_finalized = false; // needs clean up in the end
    if (ofi.exists()) {
        setPermissions(ofi.permissions()); // Ignore errors
#ifdef Q_OS_LINUX
        copyPosixAccessAcl(m_finalFilePath.toFSPathString(), fileName());
#endif
    } else {
        Permissions permAll = QFile::ReadOwner
                | QFile::ReadGroup
                | QFile::ReadOther
                | QFile::WriteOwner
                | QFile::WriteGroup
                | QFile::WriteOther;

        // set permissions with respect to the current umask
        setPermissions(permAll & ~m_umask);
    }

    return true;
}

void SaveFile::rollback()
{
    close();
    if (m_tempFile)
        m_tempFile->filePath().removeFile();
    m_finalized = true;
}

#ifndef Q_OS_WIN
// Declare what we need from Windows API so we can compile without #ifdef
constexpr int REPLACEFILE_IGNORE_MERGE_ERRORS = 0;
constexpr int ERROR_UNABLE_TO_REMOVE_REPLACED = 0;
constexpr int MOVEFILE_COPY_ALLOWED = 0;
constexpr int MOVEFILE_REPLACE_EXISTING = 0;
constexpr int MOVEFILE_WRITE_THROUGH = 0;
constexpr int FORMAT_MESSAGE_FROM_SYSTEM = 0;
constexpr int FORMAT_MESSAGE_IGNORE_INSERTS = 0;
constexpr int LANG_NEUTRAL = 0;
constexpr int SUBLANG_DEFAULT = 0;

using DWORD = unsigned long;

bool MoveFileEx(const wchar_t *, const wchar_t *, DWORD);
bool ReplaceFile(const wchar_t *, const wchar_t *, const wchar_t *, DWORD, void *, void *);
DWORD GetLastError();
DWORD MAKELANGID(int, int);
DWORD FormatMessageW(DWORD, void *, DWORD, DWORD, const wchar_t *, DWORD, void *);
#endif

bool SaveFile::commit()
{
    QTC_ASSERT(!m_finalized && m_tempFile, return false;);
    m_finalized = true;

    if (!flush()) {
        close();
        m_tempFile->filePath().removeFile();
        return false;
    }
#ifdef Q_OS_WIN
    FlushFileBuffers(reinterpret_cast<HANDLE>(_get_osfhandle(handle())));
#elif _POSIX_SYNCHRONIZED_IO > 0
    fdatasync(handle());
#else
    fsync(handle());
#endif

    close();
    if (error() != NoError) {
        m_tempFile->filePath().removeFile();
        return false;
    }

    FilePath finalFileName = m_finalFilePath.resolveSymlinks();

    if constexpr (HostOsInfo::isWindowsHost()) {
        static const bool disableWinSpecialCode = !qEnvironmentVariableIsEmpty(
            "QTC_DISABLE_SPECIAL_WIN_SAVEFILE");
        if (m_finalFilePath.isLocal() && !disableWinSpecialCode) {
            // Release the file lock
            m_tempFile.reset();
            bool result = ReplaceFile(
                finalFileName.nativePath().toStdWString().data(),
                fileName().toStdWString().data(),
                nullptr,
                REPLACEFILE_IGNORE_MERGE_ERRORS,
                nullptr,
                nullptr);
            if (!result) {
                DWORD replaceErrorCode = GetLastError();
                QString errorStr;
                if (!finalFileName.exists()) {
                    // Replace failed because finalFileName does not exist, try rename.
                    if (!(result = rename(finalFileName.toFSPathString())))
                        errorStr = errorString();
                } else {
                    if (replaceErrorCode == ERROR_UNABLE_TO_REMOVE_REPLACED) {
                        // If we do not get the rights to remove the original final file we still might try
                        // to replace the file contents
                        result = MoveFileEx(
                            fileName().toStdWString().data(),
                            finalFileName.nativePath().toStdWString().data(),
                            MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING
                                | MOVEFILE_WRITE_THROUGH);
                        if (!result)
                            replaceErrorCode = GetLastError();
                    }
                    if (!result) {
                        wchar_t messageBuffer[256];
                        FormatMessageW(
                            FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                            nullptr,
                            replaceErrorCode,
                            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                            messageBuffer,
                            sizeof(messageBuffer),
                            nullptr);
                        errorStr = QString::fromWCharArray(messageBuffer);
                    }
                }
                if (!result) {
                    remove();
                    setErrorString(errorStr);
                }
            }

            return result;
        }
    }

    const FilePath backupFile = finalFileName.stringAppended("~");

    // Back up current file.
    // If it's opened by another application, the lock follows the move.
    if (finalFileName.exists()) {
        // Kill old backup. Might be useful if creator crashed before removing backup.
        backupFile.removeFile(); // Error uninteresting.
        if (const Result<> res = finalFileName.renameFile(backupFile); !res) {
            m_tempFile->filePath().removeFile();
            setErrorString(res.error());
            return false;
        }
    }

    const Result<> renameResult = m_tempFile->filePath().renameFile(finalFileName);
    if (!renameResult) {
        // The case when someone else was able to create finalFileName after we've renamed it.
        // Higher level call may try to save this file again but here we do nothing and
        // return false while keeping the error string from last rename call.
        m_tempFile->filePath().removeFile();
        setErrorString(renameResult.error());
        backupFile.renameFile(finalFileName); // rollback to backup if possible ...
        return false; // ... or keep the backup copy at least
    }

    backupFile.removeFile();

    return true;
}

void SaveFile::initializeUmask()
{
#ifdef Q_OS_WIN
    m_umask = QFile::WriteGroup | QFile::WriteOther;
#else
    // Get the current process' file creation mask (umask)
    // umask() is not thread safe so this has to be done by single threaded
    // application initialization
    mode_t mask = umask(0); // get current umask
    umask(mask); // set it back

    m_umask = ((mask & S_IRUSR) ? QFile::ReadOwner  : QFlags<QFile::Permission>())
            | ((mask & S_IWUSR) ? QFile::WriteOwner : QFlags<QFile::Permission>())
            | ((mask & S_IXUSR) ? QFile::ExeOwner   : QFlags<QFile::Permission>())
            | ((mask & S_IRGRP) ? QFile::ReadGroup  : QFlags<QFile::Permission>())
            | ((mask & S_IWGRP) ? QFile::WriteGroup : QFlags<QFile::Permission>())
            | ((mask & S_IXGRP) ? QFile::ExeGroup   : QFlags<QFile::Permission>())
            | ((mask & S_IROTH) ? QFile::ReadOther  : QFlags<QFile::Permission>())
            | ((mask & S_IWOTH) ? QFile::WriteOther : QFlags<QFile::Permission>())
            | ((mask & S_IXOTH) ? QFile::ExeOther   : QFlags<QFile::Permission>());
#endif
}

} //  Utils
