diff --git a/src/libs/utils/devicefileaccess.cpp b/src/libs/utils/devicefileaccess.cpp index b8fe7903786..ddc82579a2a 100644 --- a/src/libs/utils/devicefileaccess.cpp +++ b/src/libs/utils/devicefileaccess.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #ifdef Q_OS_WIN #ifdef QTCREATOR_PCH_H @@ -30,6 +32,8 @@ #include #endif +#include + namespace Utils { // DeviceFileAccess @@ -397,6 +401,15 @@ void DeviceFileAccess::asyncCopyFile(const FilePath &filePath, cont(copyFile(filePath, target)); } +expected_str DeviceFileAccess::createTempFile(const FilePath &filePath) +{ + Q_UNUSED(filePath) + QTC_CHECK(false); + return make_unexpected(Tr::tr("createTempFile is not implemented for \"%1\"") + .arg(filePath.toUserOutput())); +} + + // DesktopDeviceFileAccess DesktopDeviceFileAccess::~DesktopDeviceFileAccess() = default; @@ -690,6 +703,16 @@ expected_str DesktopDeviceFileAccess::writeFileContents(const FilePath & return res; } +expected_str DesktopDeviceFileAccess::createTempFile(const FilePath &filePath) +{ + QTemporaryFile file(filePath.path()); + file.setAutoRemove(false); + if (!file.open()) + return make_unexpected(Tr::tr("Could not create temporary file in \"%1\" (%2)").arg(filePath.toUserOutput()).arg(file.errorString())); + return FilePath::fromString(file.fileName()).onDevice(filePath); +} + + QDateTime DesktopDeviceFileAccess::lastModified(const FilePath &filePath) const { return QFileInfo(filePath.path()).lastModified(); @@ -965,6 +988,65 @@ expected_str UnixDeviceFileAccess::writeFileContents(const FilePath &fil return data.size(); } +expected_str UnixDeviceFileAccess::createTempFile(const FilePath &filePath) +{ + if (!m_hasMkTemp.has_value()) + m_hasMkTemp = runInShellSuccess({"which", {"mktemp"}, OsType::OsTypeLinux}); + + QString tmplate = filePath.path(); + // Some mktemp implementations require a suffix of XXXXXX. + // They will only accept the template if at least the last 6 characters are X. + if (!tmplate.endsWith("XXXXXX")) + tmplate += ".XXXXXX"; + + if (m_hasMkTemp) { + const RunResult result = runInShell({"mktemp", {tmplate}, OsType::OsTypeLinux}); + + if (result.exitCode != 0) { + return make_unexpected( + Tr::tr("Failed creating temporary file \"%1\": %2") + .arg(filePath.toUserOutput(), QString::fromUtf8(result.stdErr))); + } + + return FilePath::fromString(QString::fromUtf8(result.stdOut.trimmed())).onDevice(filePath); + } + + // Manually create a temporary/unique file. + std::reverse_iterator firstX = std::find_if_not(std::rbegin(tmplate), + std::rend(tmplate), + [](QChar ch) { return ch == 'X'; }); + + static constexpr std::array chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', + '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + + std::uniform_int_distribution<> dist(0, chars.size() - 1); + + int maxTries = 10; + FilePath newPath; + do { + for (QChar *it = firstX.base(); it != std::end(tmplate); ++it) { + *it = chars[dist(*QRandomGenerator::global())]; + } + newPath = FilePath::fromString(tmplate).onDevice(filePath); + if (--maxTries == 0) { + return make_unexpected(Tr::tr("Failed creating temporary file \"%1\" (too many tries)") + .arg(filePath.toUserOutput())); + } + } while (newPath.exists()); + + const expected_str createResult = newPath.writeFileContents({}); + + if (!createResult) + return make_unexpected(createResult.error()); + + return newPath; +} + OsType UnixDeviceFileAccess::osType(const FilePath &filePath) const { Q_UNUSED(filePath) diff --git a/src/libs/utils/devicefileaccess.h b/src/libs/utils/devicefileaccess.h index 4b1a917ab10..8fe8201e088 100644 --- a/src/libs/utils/devicefileaccess.h +++ b/src/libs/utils/devicefileaccess.h @@ -81,6 +81,8 @@ protected: virtual void asyncCopyFile(const FilePath &filePath, const Continuation> &cont, const FilePath &target) const; + + virtual expected_str createTempFile(const FilePath &filePath); }; class QTCREATOR_UTILS_EXPORT DesktopDeviceFileAccess : public DeviceFileAccess @@ -135,6 +137,9 @@ protected: expected_str writeFileContents(const FilePath &filePath, const QByteArray &data, qint64 offset) const override; + + expected_str createTempFile(const FilePath &filePath) override; + }; class QTCREATOR_UTILS_EXPORT UnixDeviceFileAccess : public DeviceFileAccess @@ -186,6 +191,9 @@ protected: const QByteArray &data, qint64 offset) const override; + expected_str createTempFile(const FilePath &filePath) override; + + private: bool iterateWithFind( const FilePath &filePath, @@ -197,6 +205,7 @@ private: QStringList *found) const; mutable bool m_tryUseFind = true; + mutable std::optional m_hasMkTemp; }; } // Utils diff --git a/src/libs/utils/filepath.cpp b/src/libs/utils/filepath.cpp index cbc98c34786..d5853a30bad 100644 --- a/src/libs/utils/filepath.cpp +++ b/src/libs/utils/filepath.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef Q_OS_WIN #ifdef QTCREATOR_PCH_H @@ -490,6 +491,40 @@ std::optional FilePath::refersToExecutableFile(MatchScope matchScope) return fileAccess()->refersToExecutableFile(*this, matchScope); } +expected_str FilePath::tmpDir() const +{ + if (needsDevice()) { + const Environment env = deviceEnvironment(); + if (env.hasKey("TMPDIR")) + return FilePath::fromUserInput(env.value("TMPDIR")).onDevice(*this); + if (env.hasKey("TEMP")) + return FilePath::fromUserInput(env.value("TEMP")).onDevice(*this); + if (env.hasKey("TMP")) + return FilePath::fromUserInput(env.value("TMP")).onDevice(*this); + + if (osType() != OsTypeWindows) + return FilePath("/tmp").onDevice(*this); + return make_unexpected(QString("Could not find temporary directory on device %1") + .arg(displayName())); + } + + return FilePath::fromUserInput(QDir::tempPath()); +} + +expected_str FilePath::createTempFile() const +{ + if (!needsDevice()) { + QTemporaryFile file(toFSPathString()); + file.setAutoRemove(false); + if (file.open()) + return FilePath::fromString(file.fileName()); + + return make_unexpected(QString("Could not create temporary file: %1").arg(file.errorString())); + } + + return fileAccess()->createTempFile(*this); +} + bool FilePath::isReadableFile() const { return fileAccess()->isReadableFile(*this); diff --git a/src/libs/utils/filepath.h b/src/libs/utils/filepath.h index 41aaf7dcccb..d219c2cf31e 100644 --- a/src/libs/utils/filepath.h +++ b/src/libs/utils/filepath.h @@ -184,6 +184,9 @@ public: WithExeOrBatSuffix, WithAnySuffix }; std::optional refersToExecutableFile(MatchScope considerScript) const; + [[nodiscard]] expected_str tmpDir() const; + [[nodiscard]] expected_str createTempFile() const; + // makes sure that capitalization of directories is canonical // on Windows and macOS. This is rarely needed. [[nodiscard]] FilePath normalizedPathName() const; diff --git a/tests/auto/utils/fileutils/tst_fileutils.cpp b/tests/auto/utils/fileutils/tst_fileutils.cpp index e636f812159..a7272e490b1 100644 --- a/tests/auto/utils/fileutils/tst_fileutils.cpp +++ b/tests/auto/utils/fileutils/tst_fileutils.cpp @@ -115,6 +115,9 @@ private slots: void hostSpecialChars_data(); void hostSpecialChars(); + void tmp_data(); + void tmp(); + private: QTemporaryDir tempDir; QString rootPath; @@ -1325,6 +1328,36 @@ void tst_fileutils::hostSpecialChars() QCOMPARE(toFromExpected, expected); } +void tst_fileutils::tmp_data() +{ + QTest::addColumn("templatepath"); + QTest::addColumn("expected"); + + QTest::addRow("empty") << "" << true; + QTest::addRow("no-template") << "foo" << true; + QTest::addRow("realtive-template") << "my-file-XXXXXXXX" << true; + QTest::addRow("absolute-template") << QDir::tempPath() + "/my-file-XXXXXXXX" << true; + QTest::addRow("non-existing-dir") << "/this/path/does/not/exist/my-file-XXXXXXXX" << false; + + QTest::addRow("on-device") << "device://test/./my-file-XXXXXXXX" << true; +} + +void tst_fileutils::tmp() +{ + QFETCH(QString, templatepath); + QFETCH(bool, expected); + + FilePath fp = FilePath::fromString(templatepath); + + const auto result = fp.createTempFile(); + QCOMPARE(result.has_value(), expected); + + if (result.has_value()) { + QVERIFY(result->exists()); + QVERIFY(result->removeFile()); + } +} + QTEST_GUILESS_MAIN(tst_fileutils) #include "tst_fileutils.moc"