// 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 # include #else # include # include #endif namespace Utils { static QFile::Permissions m_umask; 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 } 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(_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 QString backupName = finalFileName.toFSPathString() + '~'; // 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. QFile::remove(backupName); QFile finalFile(finalFileName.toFSPathString()); if (!finalFile.rename(backupName)) { m_tempFile->filePath().removeFile(); setErrorString(finalFile.errorString()); return false; } } 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()); QFile::rename(backupName, finalFileName.toFSPathString()); // rollback to backup if possible ... return false; // ... or keep the backup copy at least } QFile::remove(backupName); 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()) | ((mask & S_IWUSR) ? QFile::WriteOwner : QFlags()) | ((mask & S_IXUSR) ? QFile::ExeOwner : QFlags()) | ((mask & S_IRGRP) ? QFile::ReadGroup : QFlags()) | ((mask & S_IWGRP) ? QFile::WriteGroup : QFlags()) | ((mask & S_IXGRP) ? QFile::ExeGroup : QFlags()) | ((mask & S_IROTH) ? QFile::ReadOther : QFlags()) | ((mask & S_IWOTH) ? QFile::WriteOther : QFlags()) | ((mask & S_IXOTH) ? QFile::ExeOther : QFlags()); #endif } } // Utils